Golf


editor=False
import asyncio
import random
import numpy as np
import pygame as pg
SimulationUpdates, SimElapesdTime, MaxSimulationSteps = 0, 0, 0
OLDCollidingPairs = []
pg.init()
clock = pg.time.Clock()
WIDTH, HEIGHT = 1400 , 800
screen = pg.display.set_mode((WIDTH+200, HEIGHT), pg.RESIZABLE)
#collidesound = pg.mixer.Sound("collide.wav")
#bopsound = pg.mixer.Sound("bop2.wav")

# Image(Surface) which will be refrenced
canvas = pg.Surface((WIDTH, HEIGHT))

if not editor:
  # Camera rectangles for sections of  the canvas
  p1_camera = pg.Rect(0,0,WIDTH//2,HEIGHT//2)
  p2_camera = pg.Rect(WIDTH//2,0,WIDTH//2,HEIGHT//2)
  p3_camera = pg.Rect(0,HEIGHT//2,WIDTH//2,HEIGHT//2)
  p4_camera = pg.Rect(WIDTH//2,HEIGHT//2,WIDTH//2,HEIGHT//2)

  # subsurfaces of canvas
  # Note that subx needs refreshing when px_camera changes.
  sub1 = canvas.subsurface(p1_camera)
  sub2 = canvas.subsurface(p2_camera)
  sub3 = canvas.subsurface(p3_camera)
  sub4 = canvas.subsurface(p4_camera)

  screens=[sub1,sub2,sub3,sub4]
else:
  # Camera rectangles for sections of  the canvas
  p1_camera = pg.Rect(0,0,WIDTH,HEIGHT)

  # subsurfaces of canvas
  # Note that subx needs refreshing when px_camera changes.
  sub1 = canvas.subsurface(p1_camera)

  screens=[sub1]

def circles_overlap(x1,y1,size1,x2,y2,size2):
  dist = (x1-x2) * (x1-x2) + (y1-y2) * (y1-y2)
  if dist <= (size1+size2) * (size1+size2):
    return True
  else:
    return False

class LineSegment:
  instances=[]
  def __init__(self,sx,sy,ex,ey,rad):
    self.sx = sx
    self.sy = sy
    self.ex = ex
    self.ey = ey
    self.radius = rad

class Object:
  instances=[]
  def __init__(self,x,y,type,num=0,rad=10):
    self.x = x
    self.y = y
    self.type = type
    self.num = num
    self.radius = rad


class Ball:
  instances=[]
  def __init__(self, x, y, size):
    self.x = x
    self.y = y
    self.vx = 0
    self.vy = 0
    self.ax = 0
    self.ay = 0
    self.size = size
    self.colour = random.randint(0,9)
    self.mass = size * 10
    self.SimTimeRemaining = 0
    self.oldx = 0
    self.oldy = 0

    self.CamTargetX = 0
    self.CamTargetY = 0
    self.CamX = 0
    self.CamY = 0
    self.keyheldtime=0
    self.keyheld=False
    self.angle = 0
    self.done = False
    self.score = 0
    self.strokes = 0
    self.name = "none"
    self.placement = 0
    self.LobbyStage = 0

  def Collide(self):
    collided=False
    for ball in Ball.instances:
      if ball != self or ball.done==True:
        if circles_overlap(self.x,self.y,self.size, ball.x,ball.y,ball.size):
          dist = ((self.x-ball.x)*(self.x-ball.x) + (self.y-ball.y)*(self.y-ball.y)) ** 0.5
          overlap = 0.5 * (dist - self.size - ball.size)

          #displace self
          self.x -= overlap * (self.x - ball.x) / dist
          self.y -= overlap * (self.y - ball.y) / dist

          #displace target
          ball.x += overlap * (self.x - ball.x) / dist
          ball.y += overlap * (self.y - ball.y) / dist
          
          return Ball.instances.index(self), Ball.instances.index(ball)
          collided=True

    
    #if collided:#time displacement
    try:
      IntendedSpeed = (self.vx*self.vx + self.vy*self.vy)**0.5
      IntendedDistance = IntendedSpeed * self.SimTimeRemaining
      ActualDistance = ((self.x-self.oldx)*(self.x-self.oldx) + (self.y-self.oldy)*(self.y-self.oldy))**0.5
      ActualTime = ActualDistance / IntendedSpeed

      self.SimTimeRemaining = self.SimTimeRemaining - ActualTime
    except:
      pass

  def Move(self):
    if self.SimTimeRemaining > 0:

      self.oldx = self.x
      self.oldy = self.y

      MoreDrag=0
      for object in Object.instances:
        if circles_overlap(self.x, self.y, (self.size//4), object.x, object.y, object.radius):
          if object.type=="water":
            MoreDrag=0.15
          elif object.type=="sand":
            MoreDrag=0.08
      if circles_overlap(self.x, self.y, (self.size//4)*3, Object.instances[4].x, Object.instances[4].y, 35):
        MoreDrag=0.04

        
      #drag to simulate rolling friction
      self.ax = -self.vx * (drag+MoreDrag)
      self.ay = -self.vy * (drag+MoreDrag) + gravity #gravity

      #update ball physics
      self.vx += self.ax * self.SimTimeRemaining / TargetFPS
      self.vy += self.ay * self.SimTimeRemaining / TargetFPS
      self.x  += self.vx * self.SimTimeRemaining / TargetFPS
      self.y  += self.vy * self.SimTimeRemaining / TargetFPS

      #move to other side of screen
      #if self.x <0:
      # self.x += WIDTH
      #if self.x >= WIDTH:
      # self.x -= WIDTH
      #if self.y <0:
      # self.y += HEIGHT
      #if self.y >= HEIGHT:
      # self.y -= HEIGHT

      if self.vx*self.vx + self.vy*self.vy < 0.0001:
        self.vx = 0
        self.vy = 0
      return self.Collide()

def MoveAll():
  global OLDCollidingPairs
  CollNoise = False
  BopNoise = False
  for i in range(SimulationUpdates):
    
    for ball in Ball.instances:
      ball.SimTimeRemaining = SimElapesdTime

    for j in range(MaxSimulationSteps):
      CollidingPairs=[]
      FakeBalls=[]
#static collisions
      for ball in Ball.instances:
        collide = True
        for i in range(len(FakeBalls)):
          if Ball.instances.index(FakeBalls[i]) == Ball.instances.index(ball):
            collide = False
        if collide:

          coll = ball.Move()
          if coll!=None:
            CollidingPairs.append(coll)

          for edge in LineSegment.instances:
            LineX1 = edge.ex - edge.sx
            LineY1 = edge.ey - edge.sy

            LineX2 = ball.x - edge.sx
            LineY2 = ball.y - edge.sy

            EdgeLength = LineX1*LineX1 + LineY1*LineY1

            t = max(0, min(EdgeLength, (LineX1*LineX2 + LineY1*LineY2))) / EdgeLength

            ClosestPointX = edge.sx + t * LineX1
            ClosestPointY = edge.sy + t * LineY1

            Distance = ((ball.x- ClosestPointX)*(ball.x- ClosestPointX) + (ball.y - ClosestPointY)*(ball.y - ClosestPointY))**0.5

            if Distance <= ball.size + edge.radius:
#static collision occoured

              Ball.instances.append(Ball(ClosestPointX,ClosestPointY,edge.radius))
              fakeball = Ball.instances[-1]

              fakeball.mass = ball.mass * 1
              fakeball.vx = -ball.vx
              fakeball.vy = -ball.vy

              #fakeball.mass = ball.mass * 1
              #fakeball.vx = -ball.vx
              #fakeball.vy = -ball.vy

              FakeBalls.append(fakeball)
              coll = Ball.instances.index(ball), Ball.instances.index(fakeball)
              CollidingPairs.append(coll)

              overlap = 1 * (Distance - ball.size - fakeball.size)

              #displace target
              try:
                ball.x -= overlap * (ball.x - fakeball.x) / Distance
                ball.y -= overlap * (ball.y - fakeball.y) / Distance
              except:
                pass

#hole colisions
          if circles_overlap(ball.x, ball.y, (ball.size//4)*3, Object.instances[4].x, Object.instances[4].y, 35):
            if circles_overlap(ball.x, ball.y, (ball.size//2), Object.instances[4].x, Object.instances[4].y, 35) and not ball.done:
              speed = 0.001
            else:
              speed = 0
            speed += np.sqrt(ball.vx*ball.vx + ball.vy*ball.vy)
            angle = np.degrees(np.arctan2(ball.vy, ball.vx))
            dx = Object.instances[4].x - ball.x
            dy = Object.instances[4].y - ball.y
            target_angle = np.degrees(np.arctan2(dy, dx))
            angle_diff = (target_angle - angle) % 360
            if angle_diff > 180:
              angle_diff -= 360

            if angle_diff > 0:
              turn = 0.08
            elif angle_diff < 0:
              turn = -0.08
            else:
              turn = 0
            angle = np.radians(angle+turn)
            ball.vx = speed * np.cos(angle)
            ball.vy = speed * np.sin(angle)

#dynamic collisions
      for i in range(len(CollidingPairs)):
        b1 = Ball.instances[CollidingPairs[i][0]]
        b2 = Ball.instances[CollidingPairs[i][1]]

        #distance
        dist = ((b1.x-b2.x)*(b1.x-b2.x) + (b1.y-b2.y)*(b1.y-b2.y)) ** 0.5

        try:#normal
          nx = (b2.x - b1.x) / dist
          ny = (b2.y - b1.y) / dist
        except:
          pass

        #tangent
        tx = -ny
        ty = nx

        #dot product tangent
        dpTan1 = b1.vx * tx + b1.vy * ty
        dpTan2 = b2.vx * tx + b2.vy * ty

        #dot product normal
        dpNorm1 = b1.vx * nx + b1.vy * ny
        dpNorm2 = b2.vx * nx + b2.vy * ny

        #conservation of momentum in 1D
        m1 = (dpNorm1 * (b1.mass - b2.mass) + 2 * b2.mass * dpNorm2) / (b1.mass + b2.mass)
        m2 = (dpNorm2 * (b2.mass - b1.mass) + 2 * b1.mass * dpNorm1) / (b1.mass + b2.mass)

        #update velocitys
        b1.vx = tx * dpTan1 + nx * m1
        b1.vy = ty * dpTan1 + ny * m1

        b2.vx = tx * dpTan2 + nx * m2
        b2.vy = ty * dpTan2 + ny * m2

        IntendedSpeed1 = (b1.vx*b1.vx + b1.vy*b1.vy)**0.5
        IntendedSpeed2 = (b2.vx*b2.vx + b2.vy*b2.vy)**0.5
        
        if IntendedSpeed1 >= 20 or IntendedSpeed2 >= 20:
          CollNoise = True
        if IntendedSpeed1 >= 10 or IntendedSpeed2 >= 10:
          BopNoise = True
          pass

      if CollNoise:
        #pg.mixer.Sound.play(collidesound)
        pass
      if BopNoise:
        #pg.mixer.Sound.play(bopsound)
        pass

      for fakeball in FakeBalls:
        Ball.instances.remove(fakeball)
      CollidingPairs=[]
      FakeBalls=[]

gridSize=100
grassColours=[(2, 158, 15),(37, 107, 39)]
grass=[
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1],
[0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1,0,1]
]
placetext=["","1st","2nd","3rd","4th"]
def DrawAll(mousemoving2, mouse_x, mouse_y):

  screen.fill((0, 0, 0))
  for SubScreen in screens:
    SubScreen.fill((73, 191, 13))
    def nx(x): # new x on screen
      return x - Ball.instances[screens.index(SubScreen)].CamX + WIDTH // 2

    def ny(y): # new y on screen
      return y - Ball.instances[screens.index(SubScreen)].CamY + HEIGHT // 2

    for x in range(len(grass)):
      for y in range(len(grass[0])):
        if nx(gridSize*x)0 and ny(gridSize*y)0:
          pg.draw.rect(SubScreen, grassColours[grass[x][y]], (nx(gridSize*x), ny(gridSize*y), gridSize, gridSize), 10)
    
    if Ball.instances[screens.index(SubScreen)].done:
      SubScreen.fill((73, 191, 13))
      font = pg.freetype.Font("prstartk.ttf", 30)
      font.render_to(SubScreen, (150,20), f"You placed {placetext[Ball.instances[screens.index(SubScreen)].placement]}", (255, 255, 255))
      font = pg.freetype.Font("prstartk.ttf", 20)
      font.render_to(SubScreen, (200,60), f"with {Ball.instances[screens.index(SubScreen)].strokes} strokes", (255, 255, 255))
    else:
      font = pg.freetype.Font("prstartk.ttf", 24)

      for object in Object.instances:
        if object.type=="start" and editor:
          pg.draw.circle(SubScreen, (255,255,255), (nx(object.x), ny(object.y)), 35)
          font.render_to(SubScreen, (nx(object.x-10), ny(object.y-10)), f"{object.num}", (0, 0, 0))

        elif object.type=="end":
          pg.draw.circle(SubScreen, (10,10,10), (nx(object.x), ny(object.y)), 35)

        elif object.type=="sand":
          pg.draw.circle(SubScreen, (236,204,162), (nx(object.x), ny(object.y)), object.radius)

        elif object.type=="water":
          pg.draw.circle(SubScreen, (28,163,236), (nx(object.x), ny(object.y)), object.radius)

      for ball in Ball.instances:
        if not ball.done:
          pg.draw.circle(SubScreen, BallColours[ball.colour], (nx(ball.x), ny(ball.y)), ball.size)
          if level=="lobby.txt":
            if ball.keyheldtime >= 20:
              pg.draw.circle(SubScreen, (255,255,255), (nx(ball.x), ny(ball.y)), ball.size+50-ball.keyheldtime//2, 5)
          else:
            if ball == Ball.instances[screens.index(SubScreen)]: #arrow direction and power
              pg.draw.line(SubScreen, (255,255,255), (nx(ball.x+np.cos(np.radians(ball.angle))*(ball.size+5)),ny(ball.y+np.sin(np.radians(ball.angle))*(ball.size+5))), 
                          (nx(ball.x+np.cos(np.radians(ball.angle))*(ball.size+20)),ny(ball.y+np.sin(np.radians(ball.angle))*(ball.size+20))), 3)
              if ball.keyheld:
                pg.draw.line(SubScreen, (255,255,255), (nx(ball.x+np.cos(np.radians(ball.angle))*-(ball.size+0)),ny(ball.y+np.sin(np.radians(ball.angle))*-(ball.size+0))), 
                        (nx(ball.x+np.cos(np.radians(ball.angle))*-(ball.size+5+ball.keyheldtime)),ny(ball.y+np.sin(np.radians(ball.angle))*-(ball.size+5+ball.keyheldtime))), 7)

      for line in LineSegment.instances:
        pg.draw.circle(SubScreen, (200, 255, 200), (nx(line.sx), ny(line.sy)), line.radius, 5)
        pg.draw.circle(SubScreen, (200, 255, 200), (nx(line.ex), ny(line.ey)), line.radius, 5)
        
        nx_val = -(line.ey - line.sy)
        ny_val = (line.ex - line.sx)
        d = (nx_val * nx_val + ny_val * ny_val) ** 0.5
        nx_val /= d
        ny_val /= d
        rad = line.radius - 5 / 2
        pg.draw.line(SubScreen, (200, 255, 200), (nx(line.sx + nx_val * rad), ny(line.sy + ny_val * rad)), (nx(line.ex + nx_val * rad), ny(line.ey + ny_val * rad)), 5)
        pg.draw.line(SubScreen, (200, 255, 200), (nx(line.sx - nx_val * rad), ny(line.sy - ny_val * rad)), (nx(line.ex - nx_val * rad), ny(line.ey - ny_val * rad)), 5)

      if level=="lobby.txt":
        pg.draw.rect(SubScreen,BallColours[Ball.instances[screens.index(SubScreen)].colour], (0,0,410,65))
        font = pg.freetype.Font("prstartk.ttf", 20)
        font.render_to(SubScreen, (10,10), f"[Tap] change colour", (0, 0, 0))
        font.render_to(SubScreen, (10,35), f"[Hold] select colour", (0, 0, 0))
        if Ball.instances[screens.index(SubScreen)].LobbyStage>0:
          font = pg.freetype.Font("prstartk.ttf", 60)
          font.render_to(SubScreen, (WIDTH//4,HEIGHT//4), f"READY", (20, 255, 20))
      
      pg.draw.rect(SubScreen,BallColours[Ball.instances[screens.index(SubScreen)].colour], (WIDTH//2-50,0,50,50))
      font = pg.freetype.Font("prstartk.ttf", 30)
      font.render_to(SubScreen, (WIDTH//2-40,12), f"{chr(PlayerKeys[screens.index(SubScreen)]-32)}", (0, 0, 0))

    if not editor:
      pg.draw.rect(SubScreen,BallColours[Ball.instances[screens.index(SubScreen)].colour], (0,0,WIDTH//2,HEIGHT//2),5)

  # draw player 1's view  to the top left corner
  screen.blit(sub1, (0,0))
  if not editor:
    # player 2's view is in the top right corner
    screen.blit(sub2, (WIDTH//2, 0))
    # player 3's view is in the bottom left corner
    screen.blit(sub3, (0, HEIGHT//2))
    # player 4's view is in the bottom right corner
    screen.blit(sub4, (WIDTH//2, HEIGHT//2))

  #countdown
  if CountdownTime>0:
    font = pg.freetype.Font("prstartk.ttf", 80)
    font.render_to(screen, (WIDTH//2-40,HEIGHT//2-40), f"{CountdownTime//60+1}", (0, 0, 0))
    font = pg.freetype.Font("prstartk.ttf", 70)
    font.render_to(screen, (WIDTH//2-35,HEIGHT//2-35), f"{CountdownTime//60+1}", (255, 255, 255))


  #------------------UI thingy hud idk
  mapoffset=0
  mapscale=10
  for line in LineSegment.instances:
    pg.draw.line(screen, (255, 255, 255), (line.sx//mapscale+WIDTH+mapoffset, line.sy//mapscale+mapoffset), (line.ex//mapscale+WIDTH+mapoffset, line.ey//mapscale+mapoffset), 2)

  for ball in Ball.instances:
    if not ball.done:
      pg.draw.circle(screen, BallColours[ball.colour], (ball.x//mapscale+WIDTH+mapoffset, ball.y//mapscale+mapoffset), ball.size//mapscale)
  
  pg.draw.line(screen, (255,255,255), (WIDTH,210), (WIDTH+200,210), 3)#divider
  font = pg.freetype.Font("prstartk.ttf", 20)
  font.render_to(screen, (WIDTH+20,220), f"Hole {level[1:4]}", (255, 255, 255))

  pg.draw.line(screen, (255,255,255), (WIDTH,248), (WIDTH+200,248), 3)#divider

  font = pg.freetype.Font("prstartk.ttf", 15)
  for player in range(4):
    pg.draw.circle(screen, BallColours[Ball.instances[player].colour], (WIDTH+20,270+player*40), 10)
    font.render_to(screen, (WIDTH+35,265+player*40), f"{Ball.instances[player].name}", (255, 255, 255))
    font.render_to(screen, (WIDTH+160,262+player*40), f"{Ball.instances[player].strokes}", (255, 255, 255))



def SaveLevel(level):
  level = open(level, "w")
  text=""
  for object in Object.instances:
    if object.type=="start":
      text += (f"s {object.x} {object.y} {object.num}\n")
    if object.type=="end":
      text += (f"e {object.x} {object.y}\n")
    if object.type=="sand":
      text += (f"p {object.x} {object.y} {object.radius}\n")
    if object.type=="water":
      text += (f"w {object.x} {object.y} {object.radius}\n")
  for line in LineSegment.instances:
    text += (f"l {line.sx} {line.sy} {line.ex} {line.ey} {line.radius}\n")
  level.write(text)
  level.close()
  print("Successfully saved :)")

def LoadLevel(level):
  global LineSegment, Ball, Object
  #try:
  LineSegment.instances=[]
  Object.instances=[]
  level = open(level, "r")
  for line in level:
    if line[0]=="s":#----------------------------start
      startX = float(line[1::].split()[0])
      startY = float(line[1::].split()[1])
      startPlayer = int(line[1::].split()[2])
      Ball.instances[startPlayer].x = startX
      Ball.instances[startPlayer].y = startY
      Object.instances.append(Object(startX,startY,"start",startPlayer))
    elif line[0]=="e":#----------------------------end
      endX = float(line[1::].split()[0])
      endY = float(line[1::].split()[1])
      Object.instances.append(Object(endX,endY,"end",0))
    elif line[0]=="p":#----------------------------sand/pit (s is alredy used idk what other letter to use so i used p)
      X = float(line[1::].split()[0])
      Y = float(line[1::].split()[1])
      rad = float(line[1::].split()[2])
      Object.instances.append(Object(X,Y,"sand",0,rad))
    elif line[0]=="w":#----------------------------water
      X = float(line[1::].split()[0])
      Y = float(line[1::].split()[1])
      rad = float(line[1::].split()[2])
      Object.instances.append(Object(X,Y,"water",0,rad))
    elif line[0]=="l":#----------------------------line
      lsX = float(line[1::].split()[0])
      lsY = float(line[1::].split()[1])
      leX = float(line[1::].split()[2])
      leY = float(line[1::].split()[3])
      lSize = int(line[1::].split()[4])
      LineSegment.instances.append(LineSegment(lsX, lsY, leX, leY, lSize))
  level.close()
  

async def Main(): 
  global SimulationUpdates, SimElapesdTime, MaxSimulationSteps, typingword, level, CountdownTime
  mousemoving0=-1
  mousemoving2=-1
  movingtype=""
  typing=0
  typingword=""



  while True:
    SimulationUpdates = 4
    MaxSimulationSteps = 5
    SimElapesdTime = clock.get_fps() / SimulationUpdates / MaxSimulationSteps

    for event in pg.event.get():
      if event.type == pg.QUIT:
        exit()

    key = pg.key.get_pressed()
    mouse_x, mouse_y = pg.mouse.get_pos()
    mouse_x, mouse_y = mouse_x + Ball.instances[0].CamX - WIDTH // 2, mouse_y + Ball.instances[0].CamY - HEIGHT // 2
    mouse_pressed = pg.mouse.get_pressed()

    if editor:
      Ball.instances[0].y += (key[pg.K_DOWN]-key[pg.K_UP]) * 30
      Ball.instances[0].x += (key[pg.K_RIGHT]-key[pg.K_LEFT]) * 30

      if key[pg.K_s] and key[pg.K_LCTRL]:
        Slevel=input("Save as:")
        SaveLevel(Slevel)

      if key[pg.K_l] and key[pg.K_LCTRL]:
        Llevel=input("Load level:")
        LoadLevel(Llevel)
        level=Llevel

      #left click
      if mouse_pressed[0]:
        if mousemoving0==-1:
          for ball in Ball.instances:
            if circles_overlap(ball.x,ball.y,ball.size,mouse_x,mouse_y,0):
              mousemoving0=Ball.instances.index(ball)
              movingtype="Ball"
          for line in LineSegment.instances:
            if circles_overlap(line.sx,line.sy,line.radius,mouse_x,mouse_y,0):
              mousemoving0=line.instances.index(line)
              movingtype="SLine"
            if circles_overlap(line.ex,line.ey,line.radius,mouse_x,mouse_y,0):
              mousemoving0=line.instances.index(line)
              movingtype="ELine"
          for object in Object.instances:
            if circles_overlap(object.x,object.y,35,mouse_x,mouse_y,0):
              mousemoving0=Object.instances.index(object)
              movingtype="Object"
      else:
        mousemoving0=-1

      if mousemoving0!=-1:
        if movingtype == "Ball":
          Ball.instances[mousemoving0].x = mouse_x
          Ball.instances[mousemoving0].y = mouse_y
        elif movingtype == "SLine":
          LineSegment.instances[mousemoving0].sx = mouse_x
          LineSegment.instances[mousemoving0].sy = mouse_y
        elif movingtype == "ELine":
          LineSegment.instances[mousemoving0].ex = mouse_x
          LineSegment.instances[mousemoving0].ey = mouse_y
        elif movingtype == "Object":
          Object.instances[mousemoving0].x = mouse_x
          Object.instances[mousemoving0].y = mouse_y

      #right click
      if mouse_pressed[2]:
        if mousemoving2==-1:
          for ball in Ball.instances:
            if circles_overlap(ball.x,ball.y,ball.size,mouse_x,mouse_y,0):
              mousemoving2=Ball.instances.index(ball)
              movingtype="Ball"
          for object in Object.instances:
            if circles_overlap(object.x,object.y,object.radius,mouse_x,mouse_y,0):
              mousemoving2=Object.instances.index(object)
              movingtype="Object"
        else:
          if movingtype=="Object":
            if Object.instances[mousemoving2].type=="sand" or Object.instances[mousemoving2].type=="water":
              Object.instances[mousemoving2].radius= np.sqrt((Object.instances[mousemoving2].x - mouse_x)*(Object.instances[mousemoving2].x - mouse_x) + (Object.instances[mousemoving2].y - mouse_y)*(Object.instances[mousemoving2].y - mouse_y))
      else:
        if mousemoving2!=-1:
          if movingtype=="Ball":
            Ball.instances[mousemoving2].vx = 0.2 * (Ball.instances[mousemoving2].x - mouse_x)
            Ball.instances[mousemoving2].vy = 0.2 * (Ball.instances[mousemoving2].y - mouse_y)
        mousemoving2=-1
    
    if CountdownTime > 0:
      CountdownTime-=1
      for ball in Ball.instances:
        ball.keyheldtime=0
        ball.keyheld=False
    else:
      if key[PlayerKeys[0]] and not Ball.instances[0].done:
        Ball.instances[0].keyheldtime+=1
        Ball.instances[0].keyheld=True
      else:
        Ball.instances[0].keyheld=False

      if key[PlayerKeys[1]] and not Ball.instances[1].done:
        Ball.instances[1].keyheldtime+=1
        Ball.instances[1].keyheld=True
      else:
        Ball.instances[1].keyheld=False

      if key[PlayerKeys[2]] and not Ball.instances[2].done:
        Ball.instances[2].keyheldtime+=1
        Ball.instances[2].keyheld=True
      else:
        Ball.instances[2].keyheld=False

      if key[PlayerKeys[3]] and not Ball.instances[3].done:
        Ball.instances[3].keyheldtime+=1
        Ball.instances[3].keyheld=True
      else:
        Ball.instances[3].keyheld=False

    if Ball.instances[0].done and Ball.instances[1].done and Ball.instances[2].done and Ball.instances[3].done:
      level=levels[levels.index(level)+1]
      LoadLevel(level)
      CountdownTime=179     
      for ball in Ball.instances:
        ball.done=False
        ball.placement=0
        ball.strokes=0
        ball.vx=0
        ball.vy=0
        ball.ax=0
        ball.ay=0
        ball.CamTargetX=ball.x
        ball.CamTargetY=ball.y
        ball.CamX=ball.x
        ball.CamY=ball.y


    if level=="lobby.txt" and Ball.instances[0].LobbyStage>0 and Ball.instances[1].LobbyStage>0 and Ball.instances[2].LobbyStage>0 and Ball.instances[3].LobbyStage>0:
      level=levels[levels.index(level)+1]
      LoadLevel(level)
      CountdownTime=179

    for ball in Ball.instances:
      if level=="lobby.txt":
        if ball.keyheldtime >= 100:
          ball.LobbyStage+=1
          ball.keyheldtime = 0
          ball.keyheld = False
        elif ball.keyheldtime != 0 and not ball.keyheld:
          if ball.LobbyStage==0:
            ball.colour+=1
            ball.colour%=len(BallColours)
          elif ball.LobbyStage==1:
            pass
        if not ball.keyheld:
          ball.keyheldtime = 0
      else:
        if ball.keyheldtime != 0 and not ball.keyheld:
          ball.vx = 0.4 * np.cos(np.radians(ball.angle)) * ball.keyheldtime
          ball.vy = 0.4 * np.sin(np.radians(ball.angle)) * ball.keyheldtime
          ball.keyheldtime = 0
          ball.strokes += 1

    #move everything
    MoveAll()


    for ball in Ball.instances:
      if not ball.keyheld:
        ball.angle+=RotateSpeed

      if circles_overlap(ball.x, ball.y, 0, Object.instances[4].x, Object.instances[4].y, 10) and np.sqrt(ball.vx*ball.vx + ball.vy*ball.vy) <= 0.4:
        ball.done=True
        ball.score+=ball.strokes
        ball.placement = sum(1 for ball in Ball.instances if ball.placement!=0)+1
        
        #for ball in Ball.instances:
          #ball.done=True

      # tx stands for target x for camera
      if ball.vx>0:
        tx=1
      elif ball.vx<0:
        tx=-1
      else:
        tx=0
      if ball.vy>0:
        ty=1
      elif ball.vy<0:
        ty=-1
      else:
        ty=0

      if editor:
        ball.CamTargetX = ball.x + tx*20
        ball.CamTargetY = ball.y + ty*20
      else:
        ball.CamTargetX = ball.x + WIDTH//4 + tx*20
        ball.CamTargetY = ball.y + HEIGHT//4 + ty*20

      ball.CamX += (ball.CamTargetX - ball.CamX) / 10
      ball.CamY += (ball.CamTargetY - ball.CamY) / 10

    #draw everything
    DrawAll(mousemoving2,mouse_x,mouse_y)

    pg.display.set_caption(f"Golf {int(clock.get_fps())}fps")
    pg.display.flip()
    clock.tick(TargetFPS)

    await asyncio.sleep(0)

PlayerKeys=[pg.K_q,pg.K_r,pg.K_u,pg.K_p]
BallColours=[(255,0,0),(0,255,0),(0,0,255),(0,255,255),(255,0,255),(255,255,0),(100,255,255),(255,100,255),(255,255,100),(50,100,255)]
TargetFPS=60
gravity = 0
drag = 0.04
RotateSpeed = 5
CountdownTime=0
if editor:
  level="l1-1.txt"
else:
  level = "lobby.txt"
levels=["lobby.txt","l1-1.txt","l1-2.txt","l1-3.txt"]
for i in range(4):
  Ball.instances.append(Ball(100+i*100,500,30))

LoadLevel(level)
asyncio.run(Main())