# PyBit.py

import turtle
import math
import keyboard
import time
from pynput.mouse import Controller, Listener
from random import *
import playsound
import ctypes
from shapely.geometry import Polygon
import threading

_mouse = Controller()

farlnd = False

_last_frame = time.time()

t = None
ui_turtle = None
ts = None

_start_time = time.time() 

Solids = []

cameraX = 0
cameraY = 0
cameraZoom = 1

Classes = {}

Backups = {}

SObjects = {}

Hidden = []

w = 0
h = 0

main = None
dist = 0

running = False

ID = 32 * randint(5, 99)

def start_engine(width, height, tt):
    global t, ui_turtle, w, h, ts, running
    import turtle
    w = width/2
    h = height/2
    turtle.setup(width, height)
    t = turtle.Turtle()
    ts = turtle.Screen()
    ts.title(tt)
    t.up()
    t.hideturtle()
    t.speed(0)
    ui_turtle = turtle.Turtle()
    ui_turtle.hideturtle()
    ui_turtle.up()
    turtle.tracer(0)
    running = True

Objects = {}
Colors = {}

Rotations = {}

def drawobject(points, name, color="black"):
    global t, Objects, Colors
    Objects[name] = points
    Colors[name] = color
    if t is None:
        bit_error("Call start_engine() first!", False)
    t.goto(*points[0])
    t.down()
    t.fillcolor(Colors[name])
    t.begin_fill()
    for pos in points[1:]:
        t.goto(*pos)
    t.goto(*points[0])
    t.end_fill()
    t.up()
    Rotations[name] = 0

def done():
    turtle.done()

def running():
    global running
    return running

def move_window(dx, dy):
    if t is None:
        bit_error("Call start_engine() first!", False)
    canvas = t.getscreen().getcanvas()
    root = canvas.winfo_toplevel()
    root.update_idletasks()
    geom = root.geometry()
    parts = geom.split("+")
    if len(parts) >= 3:
        x = int(parts[1])
        y = int(parts[2])
    else:
        x = 0
        y = 0
    new_x = int(x + dx)
    new_y = int(y - dy)
    root.geometry(f"+{new_x}+{new_y}")

def go_window(x, y):
    if t is None:
        bit_error("Call start_engine() first!", False)
    canvas = t.getscreen().getcanvas()
    root = canvas.winfo_toplevel()
    root.update_idletasks()
    geom = root.geometry()
    new_x = int(x)
    new_y = int(y)
    root.geometry(f"+{new_x}+{new_y}")

def center_camera(obj):
    global cameraX, cameraY
    cameraX = -getObjX(obj)
    cameraY = -getObjY(obj)

def timer():
    if _start_time is None:
        return 0
    return round(time.time() - _start_time, 3)

def reset_timer():
    global _start_time
    _start_time = time.time() 

def delta():
    global _last_frame
    now = time.time()
    dt = now - _last_frame
    _last_frame = now
    return dt

def dot(x, y, size=1):
    global t
    points = [(x+size, y+size), (x-size, y+size), (x-size, y-size), (x+size, y-size)]
    if t is None:
        bit_error("Call start_engine() first!", False)
    t.goto(*points[0])
    t.down()
    t.fillcolor("black")
    t.begin_fill()
    for pos in points[1:]:
        t.goto(*pos)
    t.goto(*points[0])
    t.end_fill()
    t.up()

def line(x1, y1, x2, y2):
    global t
    t.goto(x1, y1)
    t.down()
    t.goto(x2, y2)
    t.up()

def farlands():
    global farlnd
    farlnd = True

def fps():
    return 1 / delta()

def getObjects():
    return Objects

def clone(name):
    global Objects
    num = str(randint(1, 1000000))
    Objects[f"{name}clone{num}"] = Objects[name].copy()
    
    return f"{name}clone{num}"

def move(name, dx, dy):
    global t, Objects, Solids
    obj = Objects[name]
    for pos in obj:
        pos[0] += dx
        pos[1] += dy
    for i in Solids:
        if i != name and is_colliding(name, i):
                for pos in obj:
                    pos[0] -= dx
                    pos[1] -= dy
    

def breakobj(name, distortion):
    global t, Objects
    obj = Objects[name]
    for pos in obj:
        pos[0] += randint(-distortion, distortion)
        pos[1] += randint(-distortion, distortion)
    

def gravity(obj, force):
    dt = delta()
    move(obj, 0, -force * dt * 1000)

def delta_move(name, dx, dy):
    global t, Objects, Solids
    obj = Objects[name]
    dlt = delta()
    dx_scaled = dx * (dlt * 1000)
    dy_scaled = dy * (dlt * 1000)
    old = [p.copy() for p in obj]
    for pos in obj:
        pos[0] += dx_scaled
        pos[1] += dy_scaled
    for solid in Solids:
        if solid != name and solid in Objects:
            if is_colliding(name, solid):
                for i, pos in enumerate(obj):
                    pos[0] = old[i][0]
                    pos[1] = old[i][1]
                break
    

def is_colliding(name1, name2):
    obj1 = Objects[name1]
    obj2 = Objects[name2]
    x1_min = min(p[0] for p in obj1)
    x1_max = max(p[0] for p in obj1)
    y1_min = min(p[1] for p in obj1)
    y1_max = max(p[1] for p in obj1)
    x2_min = min(p[0] for p in obj2)
    x2_max = max(p[0] for p in obj2)
    y2_min = min(p[1] for p in obj2)
    y2_max = max(p[1] for p in obj2)
    if x1_max < x2_min or x1_min > x2_max: return False
    if y1_max < y2_min or y1_min > y2_max: return False
    return True

def poly_colliding(obj1, obj2):
    global Objects
    poly1 = Polygon(Objects[obj1])
    poly2 = Polygon(Objects[obj2])
    return poly1.intersects(poly2)

def move_point(obj, point, dx, dy):
    global Objects
    Objects[obj][point][0] += dx
    Objects[obj][point][1] += dy
    

def background(c):
    if c == 0:
        pass
    if c == 1:
        t.goto(-1000, 1000)
        x = -1000
        y = 1000
        t.down()
        while not y == -1000:
            while not x == 1000:
                x += 100
                t.goto(x, y)
            t.up()
            x = -1000
            y -= 100
            t.down()
    if c == 2:
        t.goto(-1000, 1000)
        t.down()
        t.goto(1000, -1000)
        t.up()
        t.goto(1000, 1000)
        t.down()
        t.goto(-1000, -1000)
        t.up()
    if c == 3:
        t.goto(0, 100)
        t.down()
        t.goto(100, 0)
        t.goto(0, -100)
        t.goto(-100, 0)
        t.goto(0, 100)
        t.up()
        t.goto(0, 200)
        t.down()
        t.goto(200, 0)
        t.goto(0, -200)
        t.goto(-200, 0)
        t.goto(0, 200)
        t.up()
        t.goto(0, 300)
        t.down()
        t.goto(300, 0)
        t.goto(0, -300)
        t.goto(-300, 0)
        t.goto(0, 300)
        t.up()

def backup(obj):
    global Backups
    Backups[obj] = getObjSprite(obj)

def getBackup(obj):
    global Backups
    return Backups[obj]

def delete(name):
    global t, Objects
    if name in Objects: del Objects[name]
    

def print_draw(text, pos, size=12, color="black"):
    global ui_turtle
    ui_turtle.goto(pos[0], pos[1])
    ui_turtle.pencolor(color)
    ui_turtle.write(text, font=("Sans Serif", size, "normal"), )
    ui_turtle.pencolor("black")

def clear_text():
    global ui_turtle
    ui_turtle.clear()

def clear_screen():
    global t
    t.clear()

def redraw_screen():
    global t, d, cameraX, cameraY, cameraZoom, Hidden, main, dist, w, h, farlnd
    t.clear()
    for obj_name, obj_points in Objects.items():
        ox = getObjX(obj_name)
        oy = getObjY(obj_name)
        p = [
            [ox + 10, oy + 10],
            [ox - 10, oy + 10],
            [ox - 10, oy - 10],
            [ox + 10, oy - 10]
        ]
        def draw():
            t.goto(obj_points[0][0] * cameraZoom + cameraX, obj_points[0][1] * cameraZoom + cameraY)
            t.down()
            t.fillcolor(Colors[obj_name])
            t.begin_fill()
            for pos in obj_points[1:]:
                if (-w <= pos[0] <= w and -h <= pos[1] <= h) or not farlnd:
                    t.goto(pos[0] * cameraZoom + cameraX, pos[1] * cameraZoom + cameraY)
                else:
                    if pos[0] < -w:
                        t.setx(-randint(int(w), 10000))
                    else:
                        t.setx(randint(int(w), 10000))
                    if pos[1] < -h:
                        t.sety(-randint(int(h), 10000))
                    else:
                        t.sety(randint(int(h), 10000))
            t.goto(obj_points[0][0] * cameraZoom + cameraX, obj_points[0][1] * cameraZoom + cameraY)
            t.end_fill()
            t.up()
        if not obj_name in Hidden:
            if ((not main == None) and (is_near(obj_name, main, dist))):
                draw()
            elif (main == None):
                draw()
            elif (not obj_name == main):
                t.goto(p[0][0] * cameraZoom + cameraX, p[0][1] * cameraZoom + cameraY)
                t.down()
                t.fillcolor("black")
                t.begin_fill()
                for pos in p:
                    t.goto(pos[0] * cameraZoom + cameraX, pos[1] * cameraZoom + cameraY)
                t.goto(p[0][0] * cameraZoom + cameraX, p[0][1] * cameraZoom + cameraY)
                t.end_fill()
                t.up()
    turtle.update()

def go(name, x, y):
    global t, Objects
    obj = Objects[name]
    cx = sum(p[0] for p in obj)/len(obj)
    cy = sum(p[1] for p in obj)/len(obj)
    dx = x - cx
    dy = y - cy
    for pos in obj:
        pos[0] += dx
        pos[1] += dy

def rotate(name, degrees):
    global Rotations
    obj = Objects[name]
    cx = sum(p[0] for p in obj) / len(obj)
    cy = sum(p[1] for p in obj) / len(obj)
    rad = math.radians(degrees)
    for pos in obj:
        x = pos[0] - cx
        y = pos[1] - cy
        x_new = x * math.cos(rad) - y * math.sin(rad)
        y_new = x * math.sin(rad) + y * math.cos(rad)
        pos[0] = x_new + cx
        pos[1] = y_new + cy
    Rotations[name] = (Rotations.get(name, 0) + degrees) % 360

def keys_down():
    kyd = []
    chars = "abcdefghijklmnopqrstuvwxyz0123456789`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/? "
    schars = ["space", "shift", "backspace", "enter"]
    for i in chars:
        if keyboard.is_pressed(i):
            kyd += [i]
    for i in schars:
        if keyboard.is_pressed(i):
            kyd += [i]
    return kyd

def is_mouse_down():
    if not hasattr(is_mouse_down, "_initialized"):
        is_mouse_down._state = False
        def _on_click(pressed):
            is_mouse_down._state = True if pressed else False
        listener = Listener(on_click=_on_click)
        listener.daemon = True
        listener.start()
        is_mouse_down._initialized = True
    try:
        return bool(ctypes.windll.user32.GetAsyncKeyState(0x01))
    except:
        return is_mouse_down._state

def exist(obj):
    global Objects
    Objects[obj] = [[0,0]]

def defeault(obj):
    global Objects
    Objects[obj] = [[10, 10], [-10, 10], [-10, -10], [10, -10]]

def window_size():
    global w, h
    return [w, h]

def fake_window_size(width, height):
    global w, h
    w = width
    h = height

def infinite_loop():
    global w, h
    clear_text()
    clear_screen()
    drawobject([[w, h], [-w, h], [-w, -h], [w, -h]], f"InfiniteLoopObjectBackground{randint(1, 100000)}")
    print_draw("∞", (-w, 0), int(round(float(w))), "white")
    print_draw("Infinite Loop!", (0, 0), int(round(float(h/10))), "white")
    done()

def too_complex():
    global w, h
    clear_text()
    clear_screen()
    drawobject([[w, h], [-w, h], [-w, -h], [w, -h]], f"TooComplexObjectBackground{randint(1, 100000)}")
    print_draw("?", (-w, 0), int(round(float(w/1.2))), "white")
    print_draw("Too Complex!", (0, 0), int(round(float(h/10))), "white")
    done()

def eng_error(sign="C", txt=""):
    global w, h
    clear_text()
    clear_screen()
    drawobject([[w, h], [-w, h], [-w, -h], [w, -h]], f"CustomErrorObjectBackground{randint(1, 100000)}")
    print_draw(sign, (-w, 0), int(round(float(w/(len(sign) * 1.2)))), "white")
    print_draw(txt, (0, 0), int(round(float(h/10))), "white")
    done()

def is_keydown(key):
    return keyboard.is_pressed(key)

def wait(seconds):
    end = time.time() + seconds
    while time.time() < end:
        turtle.update()

clicked_objects = set()

def is_clicked(name):
    screen = t.getscreen()
    clicked = [False]

    def click_handler(x, y):
        if is_touching_mouse(name):
            clicked[0] = True
    screen.onclick(click_handler)
    screen.update()
    turtle.update()
    if clicked[0]:
        clicked_objects.add(name)
        screen.onclick(None)
        return True
    return False

def set_color(name, color):
    global t, Objects
    obj = Objects[name]
    t.clear()
    for obj_name, obj_points in Objects.items():
        t.goto(*obj_points[0])
        t.down()
        t.fillcolor(color if obj_name==name else "black")
        t.begin_fill()
        for pos in obj_points[1:]:
            t.goto(*pos)
        t.goto(*obj_points[0])
        t.end_fill()
        t.up()

def getObjX(name):
    obj = Objects[name]
    return sum(p[0] for p in obj)/len(obj)

def getObjY(name):
    obj = Objects[name]
    return sum(p[1] for p in obj)/len(obj)

def is_touching_mouse(name):
    x, y = _mouse.position
    canvas = t.getscreen().getcanvas()
    root = canvas.winfo_toplevel()
    win_x = root.winfo_rootx()
    win_y = root.winfo_rooty()
    win_width = canvas.winfo_width()
    win_height = canvas.winfo_height()
    mouse_x = x - win_x - win_width//2
    mouse_y = win_height//2 - (y - win_y)
    obj = Objects[name]
    x_min = min(p[0] for p in obj)
    x_max = max(p[0] for p in obj)
    y_min = min(p[1] for p in obj)
    y_max = max(p[1] for p in obj)
    return x_min <= mouse_x <= x_max and y_min <= mouse_y <= y_max

def makeshape(name):
    recorded_points = []
    screen = t.getscreen()
    t.penup()
    def add_point(x, y):
        recorded_points.append([x, y])
        t.clear()
        if len(recorded_points) > 1:
            t.goto(recorded_points[0])
            t.pendown()
            for p in recorded_points[1:]:
                t.goto(p)
            t.penup()
    def finish_shape():
        if len(recorded_points) > 2:
            print(f"drawobject({recorded_points}, '{name}')")
            drawobject(recorded_points, name)
        else:
            print("Not enough points!")
        screen.onclick(None)
    screen.onclick(add_point, btn=1)
    screen.onclick(lambda x,y: finish_shape(), btn=3)
    import turtle
    turtle.done()

def entity(name):
    name = name.lower()
    if name == "square":
        return [[50,50],[50,-50],[-50,-50],[-50,50]]
    if name == "triangle":
        return [[-50,-50],[50,-50],[0,50]]
    if name == "octagon":
        return [[25,50],[-25,50],[-50,25],[-50,-25],[-25,-50],[25,-50],[50,-25],[50,25]]
    if name == "circle":
        points = []
        for angle in range(0, 360, 10):
            rad = math.radians(angle)
            x = 50 * math.cos(rad)
            y = 50 * math.sin(rad)
            points.append([x, y])
        return points

def scale(name, factor):
    global t, Objects
    obj = Objects[name]
    cx = sum(p[0] for p in obj)/len(obj)
    cy = sum(p[1] for p in obj)/len(obj)
    for pos in obj:
        pos[0] = cx + (pos[0]-cx)*factor
        pos[1] = cy + (pos[1]-cy)*factor
    

def mouse_x():
    x, y = _mouse.position
    canvas = t.getscreen().getcanvas()
    root = canvas.winfo_toplevel()
    win_x = root.winfo_rootx()
    win_y = root.winfo_rooty()
    win_width = canvas.winfo_width()
    win_height = canvas.winfo_height()
    mouse_x = x - win_x - win_width//2
    return mouse_x

def mouse_y():
    x, y = _mouse.position
    canvas = t.getscreen().getcanvas()
    root = canvas.winfo_toplevel()
    win_x = root.winfo_rootx()
    win_y = root.winfo_rooty()
    win_width = canvas.winfo_width()
    win_height = canvas.winfo_height()
    mouse_y = win_height//2 - (y - win_y)
    return mouse_y

def rot_between(x1, y1, x2, y2):
    return math.degrees(math.atan2(y2 - y1, x2 - x1))

def distance(x1, y1, x2, y2):
    return math.sqrt((x2 - x1)**2 + (y2 - y1)**2)

def go_forward(name, distance):
    angle = math.radians(Rotations.get(name, 0))
    dx = math.cos(angle) * distance
    dy = math.sin(angle) * distance
    obj = Objects[name]
    for p in obj:
        p[0] += dx
        p[1] += dy
    for i in Solids:
        if is_colliding(name, i):
            for p in obj:
                p[0] -= dx
                p[1] -= dy
    

def go_forward_nocollision(name, distance):
    angle = math.radians(Rotations.get(name, 0))
    dx = math.cos(angle) * distance
    dy = math.sin(angle) * distance
    obj = Objects[name]
    for p in obj:
        p[0] += dx
        p[1] += dy

def getDir(name):
    obj = Objects[name]
    if len(obj) < 2:
        return 0
    
    cx = sum(p[0] for p in obj) / len(obj)
    cy = sum(p[1] for p in obj) / len(obj)

    x1, y1 = obj[0]
    angle = math.degrees(math.atan2(y1 - cy, x1 - cx))
    return angle

def set_rot(name, degrees):
    global Objects
    obj = Objects[name]
    if len(obj) < 2:
        return

    current_angle = getDir(name)
    delta = degrees - current_angle
    rotate(name, delta - 45)

def py_delay():
    st = timer()
    et = timer()
    return et - st
    
def print_draw_main(text, pos, size=12):
    global t
    t.goto(pos[0], pos[1])
    t.write(text, font=("Sans Serif", size, "normal"), )

def area(obj):
    o = Objects[obj]
    max_x = max(p[0] for p in o)
    min_x = min(p[0] for p in o)
    max_y = max(p[1] for p in o)
    min_y = min(p[1] for p in o)
    ps = []
    y = max_y
    while y > min_y:
        x = max_x
        while x > min_x:
            ps.append([x, y])
            x -= 1
        y -= 1
    return ps

def jelly(name):
    drawobject(area(name), name)

def solid(name):
    global Solids
    Solids += [name]

def group(obj1, obj2):
    global Objects
    Objects[obj1] += Objects[obj2]
    delete(obj2)
    

def move_camera(dx, dy):
    global cameraX, cameraY
    cameraX += dx
    cameraY += dy

def scaleX(name, factor):
    global t, Objects
    obj = Objects[name]
    cx = sum(p[0] for p in obj)/len(obj)
    for pos in obj:
        pos[0] = cx + (pos[0]-cx)*factor
    

def scaleY(name, factor):
    global t, Objects
    obj = Objects[name]
    cy = sum(p[1] for p in obj)/len(obj)
    for pos in obj:
        pos[1] = cy + (pos[1]-cy)*factor
    

def split(obj):
    global Objects
    o = Objects[obj]
    max_x = max(p[0] for p in o)
    min_x = min(p[0] for p in o)
    max_y = max(p[1] for p in o)
    min_y = min(p[1] for p in o)
    ps = []
    y = max_y
    while y > min_y:
        x = max_x
        while x > min_x:
            ps.append([x, y])
            Objects[f"obj({max_x}, {max_y})"] = [[x, y]]
            x -= 1
        y -= 1
    return ps

def drag(obj, x1, y1, x2, y2, forseconds):
    go(obj, x1, y1)
    time.sleep(forseconds)
    go(obj, x2, y2)

def wiggle(obj, amount):
    move(obj, randint(-amount, amount), randint(-amount, amount))

def square(obj):
    global Objects
    o = Objects[obj]
    max_x = max(p[0] for p in o)
    min_x = min(p[0] for p in o)
    max_y = max(p[1] for p in o)
    min_y = min(p[1] for p in o)
    Objects[obj] = [[max_x, max_y], [min_x, max_y], [min_x, min_y], [max_x, min_y]]

def is_near(obj1, obj2, dist):
    if distance(getObjX(obj1), getObjY(obj1), getObjX(obj2), getObjY(obj2)) >= dist:
        return False
    else:
        return True

def not_solid(obj):
    global Solids
    del Solids[obj]

def getObjSprite(obj):
    return Objects[obj]

def change_rot(obj, value):
    global Rotations
    Rotations[obj] = value

def velX(obj, force):
    global Objects
    vel = round(force)
    while not vel == 0:
        delta_move(obj, vel, 0)
        if vel > 0:
            vel -= 1
        else:
            vel += 1

def velY(obj, force):
    global Objects
    vel = round(force)
    while not vel == 0:
        delta_move(obj, 0, vel)
        if vel > 0:
            vel -= 1
        else:
            vel += 1

def lag(precent):
    time.sleep(precent / 100)

def erase(obj):
    global t, Objects
    points = Objects[obj]
    if t is None:
        bit_error("Call start_engine() first!", False)
    t.goto(*points[0])
    t.pencolor("white")
    t.down()
    t.fillcolor("white")
    t.begin_fill()
    for pos in points[1:]:
        t.goto(*pos)
    t.goto(*points[0])
    t.end_fill()
    t.up()
    t.pencolor("black")
    turtle.update()

def bit_update():
    turtle.update()

def say(obj, say, objscale):
    print_draw(say, (getObjX(obj), getObjY(obj) + objscale))

def play_sound_bit(file):
    threading.Thread(target=playsound.playsound, args=(file,), daemon=True).start()

def ask(question, size=42):
    global w, h
    ans = ""
    finished = False
    t.goto(w, -h/1.5)
    t.down()
    t.goto(-w, -h/1.5)
    t.up()
    print_draw(question, (-w + 10, -h/1.1), size)
    print_draw("Press 'Enter' to enter your answer", (-w + 10, -h/1.35), 15)
    display = turtle.Turtle()
    display.hideturtle()
    display.penup()
    display.goto(-w + 10, -h/1.4)
    keys = []
    def key_press(char):
        keys.append(char)
    def backspace():
        keys.append("BackSpace")
    def submit():
        keys.append("Return")
    screen = turtle.Screen()
    screen.listen()
    for c in "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 .,!?+=/\\%@#$%^&*()_[];:,'`|{}":
        screen.onkey(lambda ch=c: key_press(ch), c)
    screen.onkey(backspace, "BackSpace")
    screen.onkey(submit, "Return")
    def input_loop():
        nonlocal ans, finished
        if keys:
            key = keys.pop(0)
            if key == "Return":
                finished = True
                return
            elif key == "BackSpace":
                ans = ans[:-1]
            else:
                ans += key
            display.clear()
            display.goto(-w + 10, -h/1.5)
            display.write(ans, font=("Arial", 17, "normal"))
        screen.ontimer(input_loop, 50)
    input_loop()
    while not finished:
        screen.update()
    clear_text()
    display.clear()
    return ans

def help_bit():
    print("use drawobject() to draw a object, you can use makeshape() to make a shape and import it")
    print("move objects with move()")
    print("Okay youre ready.")

def looking_at(obj, max_dist, offset=0, pscale=0.1):
    drawobject(entity("square"), "pointer")
    scale("pointer", pscale)
    hide("pointer")
    go("pointer", getObjX(obj), getObjY(obj))
    change_rot("pointer", getDir(obj) + (50 + offset))
    while True:
        if not is_near(obj, "pointer", max_dist):
            break
        else:
            go_forward_nocollision("pointer", 10)
            for i in Objects.keys():
                if (not i == obj) and (not i == "pointer"):
                    if is_colliding("pointer", i):
                        return i

def player1(obj, speed):
    dx = 0
    dy = 0
    if is_keydown("w"):
        dy += speed
    if is_keydown("s"):
        dy -= speed
    if is_keydown("a"):
        dx -= speed
    if is_keydown("d"):
        dx += speed
    delta_move(obj, dx, dy)
    return [dx, dy]

def player2(obj, speed):
    dx = 0
    dy = 0
    if is_keydown("w"):
        dy += speed
    if is_keydown("s"):
        dy -= speed
    if is_keydown("a"):
        dx -= speed
    if is_keydown("d"):
        dx += speed
    rotate(obj, -dx)
    go_forward(obj, dy)
    return [dx, dy]

def PlayerClass(obj):
    global Classes
    Classes["Player"] = [None, [obj]]

def Class(name, objects, function):
    global Classes
    Classes[name] = [function, objects]

def UptClasses(classname=None, amount=0):
    global Classes
    players = Classes.get("Player", [None, []])[1]
    classes_to_check = [classname] if classname else Classes.keys()
    for cname in classes_to_check:
        if cname == "Player":
            continue
        behavior, objs = Classes[cname]
        for obj in objs:
            for player_obj in players:
                if is_colliding(obj, player_obj):
                    if behavior == "item":
                        go(obj, 0, 0)
                        wiggle(obj, amount)
                    elif behavior == "death":
                        delete(player_obj)
                        exit()
                    elif behavior == "connect":
                        group(player_obj, obj)
                        drawobject([[0, 0]], obj)
                    elif behavior == "bounce":
                        wiggle(player_obj, amount)
                    elif behavior == "goal":
                        print("YOU WIN!")
                        exit()
                    elif behavior == "coin":
                        delete(obj)
                    elif behavior == "boom":
                        delete(obj)
                        delete(player_obj)
                        drawobject([[0, 0]], obj)
                        drawobject([[0, 0]], player_obj)
                    elif behavior == "stick":
                        go(obj, getObjX(player_obj), getObjY(player_obj))
                    elif behavior == "glass":
                        breakobj(obj, amount)
                if behavior == "walk":
                    wiggle(obj, amount)
                if behavior == "enemy_walk":
                    wiggle(obj, amount)
                    if is_colliding(obj, player_obj):
                        delete(player_obj)
                        exit()

def moveClass(classname, dx, dy):
    global Classes
    for i in Classes[classname][1]:
        move(i, dx, dy)

def runClass(classname, run, objs):
    global Classes
    for i in Classes[classname][1]:
        eval(run.replace(objs, i))

def groupClass(classname):
    global Classes
    objects = Classes[classname][1]
    if not objects: return
    base = objects[0]
    for obj in objects[1:]:
        group(base, obj)
    Objects[f"{classname}_grouped"] = Objects.pop(base)

def bit_error(txt, screen_error=True):
    global Objects, w, h
    if screen_error:
        for i in range(len(Objects.keys())):
            delete(list(Objects.keys())[0])
        clear_screen()
        clear_text()
        print_draw(f"A error has occured: {txt}", (-w + (w/100),0), round((w/10)-len(txt)/2))
        done()
    print(f"PyBit Error: {txt}")

def critical_bit_error(txt, screen_error=True):
    global Objects, w, h
    if screen_error:
        for i in range(len(Objects.keys())):
            delete(list(Objects.keys())[0])
        clear_screen()
        clear_text()
        print_draw(f"Critical Error: {txt}", (-w + (w/100),0), round((w/10)-len(txt)/2))
        done()
    print(f"PyBit Error (Critical): {txt}")

def shutdown_bit_error(txt):
    global Objects, w, h
    turtle.bye()
    print(f"PyBit Error (Shutdown): {txt}")
    print("There was a big error that caused the program to shutdown")
    print("Dont worry nothing was damaged, but if this keeps happening again report it or look for any errors in your code")

def light_bit_error(txt):
    print(f"PyBit Error (Warning): {txt}")

def camera_bye():
    global w, h
    drawobject([[w, h], [-w, h], [-w, -h], [w, -h]], f"camera_gone_obj{randint(0, 10000000)}")
    done()

def cursor(obj):
    go(obj, mouse_x(), mouse_y())

def bit_bye():
    bit_error("Forcefully stopped simulation", True)

def no_piracy():
    c = randint(0, 100)
    print(c)
    r = ask("We sent a number to the terminal, please enter it.", 20)
    if int(r) == c:
        pass
    else:
        bit_error("Failed piracy test.", True)

def boom():
    exit()

def insta_eng():
    start_engine(800, 800, "")

def save_state():
    global Objects, SObjects
    SObjects = Objects.copy()

def last_state():
    global Objects, SObjects
    Objects = SObjects.copy()

def idea(goal=False):
    subjects = ["Player", "Enemies", "Items", "The goal", "The level", "Gravity", "Time", "The world"]
    verbs = ["is", "can be", "becomes", "turns", "will be", "suddenly becomes"]
    adjectives = ["fast", "slow", "tiny", "huge", "invisible", "deadly", "sticky", "bouncy", "glitchy", "randomized", "unstable", "explosive"]
    mechanics = [
        "when you jump",
        "every 10 seconds",
        "when you touch something",
        "at random times",
        "when you look away",
        "when you stop moving",
        "when your health is low"
    ]
    goals = [
        "reach the exit",
        "survive as long as possible",
        "collect all items",
        "defeat a boss",
        "escape before time runs out",
        "protect an NPC"
    ]
    twists = [
        "but controls are reversed",
        "but everything is upside down",
        "but you only have one life",
        "but the map keeps changing",
        "but enemies copy your moves",
        "but you can't stop moving"
    ]
    sentence = f"{choice(subjects)} {choice(verbs)} {choice(adjectives)} {choice(mechanics)}."
    if goal:
        sentence += f" goal: {choice(goals)}."
    sentence += f" {choice(twists).capitalize()}."
    print(sentence)

def connect(obj1, obj2):
    drawobject([getObjCenter(obj1), getObjCenter(obj2)], f"con_{obj1}_{obj2}")

def filter(text=""):
    ct = text.lower().replace(" ", "").replace("!", "i")
    ban = ["stupid", "dumb", "idiot", "suck", "fool", "loser", "bad", "hate", "kill", "die", "death", "damn", "crap", "shit", "fuck", "bitch", "ass", "nigg", "whore"]
    for word in ban:
        if word in ct:
            return "X" * len(text)
    return text

def bimbimbambam():
    eng_error("B", "bimbimbambam")

def look_at_mouse(obj):
    angle = math.degrees(math.atan2(mouse_y() - getObjY(obj), mouse_x() - getObjX(obj)))
    set_rot(obj, angle)

def look_at(obj1, obj2):
    angle = math.degrees(math.atan2(getObjY(obj2) - getObjY(obj1), getObjX(obj2) - getObjX(obj1)))
    set_rot(obj1, angle)

def navigate(obj1, obj2, speed):
    if getObjX(obj1) > getObjX(obj2):
        move(obj1, -speed, 0)
    if getObjX(obj1) < getObjX(obj2):
        move(obj1, speed, 0)
    if getObjY(obj1) > getObjY(obj2):
        move(obj1, 0, -speed)
    if getObjY(obj1) < getObjY(obj2):
        move(obj1, 0, speed)

def nav_go(obj, x, y, speed):
    if getObjX(obj) > x:
        move(obj, -speed, 0)
    if getObjX(obj) < x:
        move(obj, speed, 0)
    if getObjY(obj) > y:
        move(obj, 0, -speed)
    if getObjY(obj) < y:
        move(obj, 0, speed)

def nav_delta_go(obj, x, y, speed):
    if getObjX(obj) > x:
        delta_move(obj, -speed, 0)
    if getObjX(obj) < x:
        delta_move(obj, speed, 0)
    if getObjY(obj) > y:
        delta_move(obj, 0, -speed)
    if getObjY(obj) < y:
        delta_move(obj, 0, speed)

def hide(obj):
    global Hidden
    Hidden += [obj]

def show(obj):
    global Hidden
    Hidden.remove(obj)
       

def no_hide(obj):
    global Hidden
    for i in range(len(Hidden)):
        if Hidden[i] == obj:
            Hidden[i] = ""

def getObjCenter(obj):
    return [getObjX(obj), getObjY(obj)]

def roundObj(obj):
    global Objects
    r = []
    for p in Objects[obj]:
        p[0] = round(p[0])
        p[1] = round(p[1])
        r += [p]
    Objects[obj] = [r]

def wait_until(x):
    while not x:
        pass

def main_bit(obj, maxdist):
    global main, dist
    main = obj
    dist = maxdist

def tragic_bye_obj(obj):
    print(f"[{obj}]: No! Please No!")
    print(f"[{obj}]: Why would you do this???")
    print(f"[{obj}]: NOOOOOOOO!")
    print(f"{obj} - Lived from 0 to {timer()}")
    delete(obj)

def setCameraZoom(scale):
    global cameraZoom
    cameraZoom = scale
    

def particle(obj, count=20, speed=1, scale=5, max_dist=100):
    for i in range(count):
        drawobject([[getObjX(obj) + scale, getObjY(obj) + scale], [getObjX(obj) - scale, getObjY(obj) + scale], [getObjX(obj) - scale, getObjY(obj) - scale], [getObjX(obj) + scale, getObjY(obj) - scale]], f"particle_{obj}_{i}")
        change_rot(f"particle_{obj}_{i}", randint(0, 360))
        while is_near(f"particle_{obj}_{i}", obj, max_dist):
            go_forward_nocollision(f"particle_{obj}_{i}", speed)
        delete(f"particle_{obj}_{i}")

def make_border():
    global w, h
    drawobject([[w, h], [-w, h]], f"border_1")
    drawobject([[w, h], [w, -h]], f"border_2")
    drawobject([[-w, h], [-w, -h]], f"border_3")
    drawobject([[w, -h], [-w, -h]], f"border_4")
    solid("border_1")
    solid("border_2")
    solid("border_3")
    solid("border_4")

def rem_border():
    delete("border_1")
    delete("border_2")
    delete("border_3")
    delete("border_4")

def getObjSize(name):
    obj = Objects[name]
    xs = [p[0] for p in obj]
    ys = [p[1] for p in obj]
    width = max(xs) - min(xs)
    height = max(ys) - min(ys)
    return width, height

def goodbye():
    turtle.bye()

def ASCII(res=5):
    img = []
    for y in range(-400, 401, res):
        line = ""
        for x in range(-400, 401, res):
            found = False
            for name in Objects:
                ox, oy = getObjCenter(name)
                ow, oh = getObjSize(name)
                if (ox - ow/2 < x < ox + ow/2) and (oy - oh/2 < y < oy + oh/2):
                    found = True
                    break
            line += "⬛" if found else "⬜"
        img += [line]
    i = len(img) - 1
    while i >= 0:
        print(img[i])
        i -= 1

def scaleXY(obj, factorX, factorY):
    scaleX(obj, factorX)
    scaleY(obj, factorY)

def runBit(code):
    global Objects
    c = code.split(" ")
    if c[0] == "run":
        while not getObjCenter(c[1]) == [int(c[2]), int(c[3])]:
            nav_go(c[1], int(c[2]), int(c[3]), int(c[4]))
    if c[0] == "sign":
        print_draw(c[1], (0, 0), int(c[2]))
    if c[0] == "death":
        delete(c[1])
    if c[0] == "say":
        print(f"[{c[1]}]: {filter(c[2])}")
    if c[0] == "mot":
        move(c[1], int(c[2]), int(c[3]))
    if c[0] == "scale":
        scale(c[1], int(c[2]))
    if c[0] == "tp":
        go(c[1], int(c[2]), int(c[3]))
    if c[0] == "create":
        drawobject(entity(c[2]), c[1])
    if c[0] == "wait":
        wait(round(c[1]))
    if c[0] == "let":
        if c[2] == "go":
            go(c[1], int(c[3]), int(c[4]))
        elif c[2] == "be":
            Objects[c[1]] = eval(c[3])
        elif c[2] == "come":
            while not getObjCenter(c[1]) == [int(c[3]), int(c[4])]:
                nav_go(c[1], int(c[3]), int(c[4]), int(c[5]))
                redraw_screen()
        elif c[2] == "come_to":
            while not is_near(c[1], c[3], int(c[4])):
                nav_go(c[1], c[3], int(c[4]))
                redraw_screen()
        elif c[2] == "die":
            delete(c[1])

# Object class
def objectClass(obj):
    class obj_cls:

        def __init__(self):
            self.name = obj
            self.moves = []
        
        def move(self, dx, dy):
            move(self.name, dx, dy)
            self.moves += [[dx, dy]]
        
        def movement(self, speed):
            self.moves += [player1(self.name, speed)]
        
        def run(self, code):
            try:
                exec(code)
            except:
                delete(self.name)
        
        def speak(self, txt):
            print(f"[{self.name}]: {txt}")
        
        def solid(self):
            solid(self.name)
        
        def not_solid(self):
            not_solid(self.name)
        
        def scale(self, factor):
            scale(self.name, factor)
            self.moves += [[factor]]
        
        def goto(self, x, y):
            go(self.name, x, y)
            self.moves += [[x, y, 0]]
        
        def walk(self, speed):
            wiggle(self.name, speed)
            self.moves += [[0, 0]]
        
        def deadly(self):
            global Objects
            CObjects = Objects.copy()
            for i in CObjects.keys():
                if not self.name == i:
                    if is_colliding(self.name, i):
                        delete(i)
                        exist(i)
        
        def rotate(self, degrees):
            rotate(self.name, degrees)
        
        def path(self, obj, speed):
            navigate(self.name, obj, speed)
        
        def path_pos(self, x, y, speed):
            nav_go(self.name, x, y, speed)
        
        def bye(self):
            delete(self.name)
            exist(self.name)
        
        def undo(self):
            l = len(self.moves) - 1
            while not l == 0:
                i = self.moves[l]
                if len(i) == 1:
                    scale(self.name, 1 / i[0])
                if len(i) == 2:
                    move(self.name, -i[0], -i[1])
                if len(i) == 3:
                    go(self.name, i[0], i[1])
                l -= 1
            self.moves = []
        
        def clear_mem(self):
            self.moves = []

    return obj_cls()

# Mobile stuff
class mobile:

    var_joy_x = 0
    var_joy_y = 0
    var_joy_size = 0

    def joystick(size=1.5):
        global w, h
        drawobject(entity("circle"), "mob_joy_main", "gray")
        go("mob_joy_main", -w/1.4, -h/1.4)
        scale("mob_joy_main", size)
        mobile.var_joy_size = size
        drawobject(entity("circle"), "mob_joy_inner")
        scale("mob_joy_inner", 0.5)
        go("mob_joy_inner", -w/1.4, -h/1.4)
    
    def joy_upt():
        if is_mouse_down() and is_touching_mouse("mob_joy_main"):
            mx, my = mouse_x(), mouse_y()
            cx, cy = getObjX("mob_joy_main"), getObjY("mob_joy_main")
            dx = mx - cx
            dy = my - cy
            max_radius = mobile.var_joy_size * 50
            dist = math.sqrt(dx**2 + dy**2)
            if dist > max_radius:
                scale = max_radius / dist
                dx *= scale
                dy *= scale
                mx = cx + dx
                my = cy + dy
            go("mob_joy_inner", mx, my)
            mobile.var_joy_x = dx / max_radius
            mobile.var_joy_y = dy / max_radius
        elif is_mouse_down():
            go("mob_joy_inner", getObjX("mob_joy_main"), getObjY("mob_joy_main"))
            set_rot("mob_joy_inner", rot_between(getObjX("mob_joy_inner"), getObjY("mob_joy_inner"), mouse_x(), mouse_y())+45)
            go_forward_nocollision("mob_joy_inner", mobile.var_joy_size * 50)
        else:
            go("mob_joy_inner", *getObjCenter("mob_joy_main"))
    
    def joydistX():
        return getObjX("mob_joy_inner") - getObjX("mob_joy_main")

    def joydistY():
        return getObjY("mob_joy_inner") - getObjY("mob_joy_main")
    
    def joy_angle():
        if is_mouse_down():
            dx = getObjX("mob_joy_inner") - getObjX("mob_joy_main")
            dy = getObjY("mob_joy_inner") - getObjY("mob_joy_main")
            angle = math.degrees(math.atan2(dy, dx))
            return angle
        else:
            return 0
    
    def joy_dist():
        return distance(*getObjCenter("mob_joy_main"), *getObjCenter("mob_joy_inner"))
    
    def joy_data_see():
        print(f"joyX: {mobile.joydistX()} joyY: {mobile.joydistY()}")
    
    def joy_reset():
        go("mob_joy_inner", getObjX("mob_joy_main"), getObjY("mob_joy_main"))
        mobile.var_joy_x = 0
        mobile.var_joy_y = 0
    
    def disable():
        delete("mob_joy_main")
        delete("mob_joy_inner")

# For Testing
def run_info():
    print("Objects:", Objects)
    print("Solids:", Solids)
    print("FPS:", fps())
    print("Delta:", delta())
    print("Camera:", cameraX, "-", cameraY)
    print("OTSR:", "0" if len(Solids) == 0 else Solids/Objects)

def calib_timer(calib_time=1):
    global _start_time
    s = timer()
    wait(calib_time)
    e = timer()
    r = e - s
    if r != 1:
        r -= 1
        _start_time -= r

def bitrand(x, mod=100):
    try:
        return x * (math.pi**math.e / math.e - math.sqrt(
            math.sin(math.e) / math.cos(
                math.pi / math.e * x % 50 / (x / x**2)
            ) * x * math.e / -math.tan(math.e)
        )) * ((timer()+1)*10) % mod
    except:
        return x * (math.pi**math.e / math.e - math.sqrt(
            math.sin(math.e) / math.cos(
                math.pi / math.e % 50 / (math.pi**2)
            ) * x * math.e / -math.tan(math.e)
        )) * ((timer()+1)*10) % mod

def timer_offset():
    s = timer()
    e = timer()
    return e - s

def get_id():
    global ID
    return ID

def change_id(x):
    global ID
    ID = x
    if ID % 32 != 0:
        print("This ID does not pass the engine piracy test, Be carefull when using this ID")

def offical():
    if ID % 32 == 0:
        return True
    return False

def no_eng_piracy():
    if not offical():
        critical_bit_error("This is not the offical version of PyBit")

def no_eng_piracy_plus():
    if ID % 32 == 0:
        if ID % 16 == 0:
            if ID % 8 == 0:
                if ID % 4 == 0:
                    if ID % 2 == 0:
                        pass
                    else:                        critical_bit_error("This is not the offical version of PyBit")
                else:                    critical_bit_error("This is not the offical version of PyBit")
            else:                critical_bit_error("This is not the offical version of PyBit")
        else:            critical_bit_error("This is not the offical version of PyBit")

no_eng_piracy_plus()