Metadata-Version: 2.4
Name: skyport_engine
Version: 0.2.25
Summary: a 2D game engine built off of pygame-ce
Author: Matthew R
Author-email: matthew.le.robins@gmail.com
Description-Content-Type: text/markdown
Requires-Dist: pygame-ce>=2.5.5
Requires-Dist: numpy>=2.3.2
Dynamic: author
Dynamic: author-email
Dynamic: description
Dynamic: description-content-type
Dynamic: requires-dist
Dynamic: summary

made by: Matthew R and William L
```
///////////////////////////////////
-----welcome to skyport_engine-----
///////////////////////////////////
```
( semmi stable )
 
skyport is a 2D game engine built on pygame-ce that simplifies tasks like managing the window , asset loading , rendering and eventually more.
# Data :
## links :
- GitHub : https://github.com/ultimate-jeff/skyport_engine
- pypi : https://pypi.org/project/skyport-engine/

## Table of Contents
- Data : (like links or other data)
- getting started : (like a tutorial on how to use skyport)
- examples : (this would hold examples and code snipits)
- Content : (like all classes in skyport_engine)
- dev_info : (like email ...)

## what platforms does skyport_engine work on :
- windows (this is the platform it is developed on)
- linux (slight testing has been done on linux)
- mac (has not been tested yet but might work)

# features :
- Multithreaded rendering
- Automatic window management
- Asset preloading and caching
- Built-in input handling
- Delta timer
- Render helper class / rendering pipe line
- Experimental chunk renderer
- SDL2 renderer (experimental)

# Getting started:
Note:
    skyport is not a replacement for pygame, so it is good to be familiar with pygame.
   
to start you will first need skyport_engine
```terminal
pip install skyport-engine
```
## Boilerplate code:
```python
import skyport as sp
 
dm = sp.Display_Manager(
    window_size=(1920,1080),
    display_size=(960,540),
    window_name="skyport test"
    )
 
dm.START_RENDERING_THREAD(fps=60)
while dm.running:
 
    dm.event_handler()
```

## Creating the window:
To start off you might want to have pygame and skyport, but don't import pygame directly â€” otherwise you will be using a separate instance of pygame from the rest of skyport. Instead, get pygame from skyport:
```python
import skyport as sp
 
pygame = sp.pygame
```
Now you can use pygame and skyport normally.
 
To create your window you will need to create a Display_Manager instance:
```python
 
    display_manager = sp.Display_Manager(
        window_size=(500, 500),
        display_size=(100, 100), # the display size is the scale at which the game is run
        window_name="my window"
        window_ico=None # this would be a pygame.Surface or an image, but we will go over this later
        force_full_screen=False # this determines if the window will start full screened
        resizable=True # if this is False then the window will not be resizable
    )
```
This will make a window pop up, but it won't respond, because the rendering thread is not on and you have to manually manage the window in this state.
 
Note :
skyport has some resolution templates such as
```python
skyport.REZ_1080p

skyport.RES_4k
```
all the resolution presets are skyport.REZ_...

One thing you can do while the window is in this state is manually run a loading screen while your game initializes before the rendering thread starts:
```python
import time
pygame = sp.pygame
 
# you will see how to use the loader further down in the README.md
loader = sp.Loader(__file__)
img = loader.read(path="C:\\Users\\matt\\source\\repos\\RSPG\\RSPG\\pt-17.png",add_to_map=True)
 
 
dm = sp.Display_Manager(
    window_size=(500,500),
    display_size=(100,100),
    window_name="my first skyport window",
    window_ico=img
)
 
time.sleep(10) # your game loading
 
# until you start the rendering thread the window will not respond
dm.START_RENDERING_THREAD(60)
 
sp.loger.output_print_data() # this line prints out everything that the engine has logged, including any errors or random logs
 
#game loop
while dm.running:
 
    dm.event_handler()
    dm.tick(tps=20) # <- this just ensures that the game loop runs at 20 ticks per second
```
In this example the window will not respond for 10 seconds, but in that time you could initialize your game on a separate thread and run a loading screen (video on threading: https://www.youtube.com/watch?v=A_Z1lgZLSNc).
 
## Game loop:
After you have the window created:
```python
import skyport as sp
import time
pygame = sp.pygame
 
dm = sp.Display_Manager(
    window_size=(500,500),
    display_size=(100,100),
    window_name="my first skyport window",
    ....
)
```
You can create the game loop, and with skyport it's simple:
```python
#previous code like creating the display .....
 
display_manager.START_RENDERING_THREAD(fps=60) # <- this starts the rendering thread
 
while display_manager.running:
 
    display_manager.event_handler() # <- this handles window events and keybinds; if it is not in the game loop the window will not respond
 
    # this line (\/) is not needed, but it ensures that the game loop has consistent timing (in this case at 20 ticks per second)
    display_manager.tick(tps=20)
```
Creating the game loop is very simple.
## keybinds and event handler :
there are 3 types of binds : key up , key down , key pressed
these you can bind any function to any of the 3 types and you can have multiple functions on 1 key
### adding and removing binds :
to add or to remove a bind you can call its add or remove method for the type
```python
display_manager.add_keyup_bind(pygame.K_a,my_function)

display_manager.add_keydown_bind(pygame.K_s,lambda : print("this bind works"))

dislay_manager.add_keypressed_bind(pygame.K_e,[func1,func2,func3]) # <- you can add multiple functions to 1 key and it works for all bind types not just pressed keys
```
you can also remove binds
```python
display_manager.remove_keyup_bind(pygame.K_a,my_function) # this will remove the function from the key bind of 'a'
display_manager.remove_keydown_bind(pygame.K_s,function)

display_manager.remove_keypressed_bind(pygame.K_e,[func1,func2,func3]) #<- you can also remove multiple binds and this works on the other types as well
```
you might want to use mouse position on some of your games and to do that you would use display_manager.get_mouse_pos() and this would return a tuple of x and y of the mouse pos (not of actual pos but pos relative to the display not window)
```python
mouse_pos = display_manager.get_mouse_pos() # this returns (x,y)
```
### how binds get triggered :
for your keybinds to work in the game loop the event handler must be called
```python
import skyport
pygame = skyport.pygame

# ... other code ...

clock = pygame.time.Clock() # you don't have to use the Display_Manager's built in clock for your game loop

while display_manager.running:

    display_manager.event_handler() # this will take care of keybinds and other events

    clock.tick(20) # <-tps
```
### joysticks and gamepad and other input methods :
skyport will automatically handle adding and removing of joysticks / controlors

to add a bind to a joystick button its just like keybinds but the function you pass in needs take in 1 parameter of the controller id

```python
display_manager.add_joy_button_down_bind(button_index=9,funcs=lambda joy_index : ... )
```
and then removing a joystick bind is the same as removing a keybind

more controller / joystick support will come later

## blitting and filling of the window :
note :
```text
there are 2 surfaces in the display manager 1: the window and 2: the display
the display is the one that you will blit to (the display is scaled to the size of the window then the display is blitted to the window)
```
there are 3 main methods :
 - blit(self,source: "pygame.Surface", dest: "pygame.RectLike" = (0, 0), area: "pygame.RectLike" = None, special_flags: "int" = 0):
 - fill(self,color:"tuple"=(0,0,0,0),rect:"pygame.Rect"=None,special_flags:"int"=0):
 - get_display() -> display_manager_instance display
these three methods are currently all you have to work with but because you can get the display all pygame ops work
```python
# .....
display = display_manager.get_display()

display.fill((100,0,0))

display_manager.blit(surf,(10,10)) # works just like the pygame blit (bc it is)

# .....
```
do take into account that when you blit something to the display it will not go on to the window unless the rendering thread is started or you would have to call display_manager.update_window()
```python
# ... previous code /\
display_manager.blit(surf,(10,10)) # blit a surf to pos x=10,y=10

display_manager.update_window() # update window so what was just blitted to the display is now on the window

display_manager.START_RENDERING_THREAD(fps=60) # the rendering thread automatically handles updating the display so after you start the rendering thread you should not call update_window()
#... game loop \/
```

## how to use the Loader :
note :
```text
the loader is made to pre load files so your game is not waiting on ssd to load your files
```
creating a Loader instance is very easy
```python

loader = skyport.Loader(__file__) # you need to pass this in or the loader might not use the correct base directory for relative paths

```
to actually pre load your loader instance with files you will need to call loader.load_from_map
```python
loader = skyport.Loader(__file__)

loader.load_from_map(map_path="path/to/Loader_preload_map.json")
```
when you call load_from_map the loader instance will look at that file dir and try to preload all the files that the loader map said to pre load

note :
```text
to see how to make a Loader map go to the Loader section in the read me and in a later update there will be a tool to automatically create Loader maps for you
```

### how to read files with the Loader :
the method for trading a file with the loader is loader.read
```python

image = loader.read(path="my_images/file.png") # this will return the files content and in this case its a image

data = loader.read(path="my_files/data.json",add_to_map=True) # in this case i want to load the json more than once so i will att it to the map which means next time you try to load it it will pull it from memory instead of disk

```
there is also some other ways to read a file but these ways have an error asset tied to them so if your image fails to load you get an error image
```python
skyport.Loader.init() # this will init the Loader class with the error assets

loader = skyport.Loader(__file__) # create a Loader instance to load files

img = loader.image(path="my_images/jeff.png") # if the loader cant load this file then it will return an error image
sound = loader.sound(path="my_sounds/idk.mp3") # there is also an error sound

data = loader.data(path="my_data/dave.json") # there is only a error json (try to not use this one but it exists)
```
the Loaders .image , .sound , .data methods all have the same parameters as loader.read but they will try to give you an error asset instead of None
### saving files with the Loader :
you can save files with loader.save(path,map_key)
```python
img = pygame.Surface((100,100))
img.fill((100,100,100))


loader.save(img,"my_images/new_img.jpeg")


```

## more will come ...
as of the current version the documentation on how to get started with skyport is not complete and will be in later updates

# examples :

## example 1 basic code :
```python
import skyport as sp
pygame = sp.pygame

dm = sp.Display_Manager(
    window_size=(500,500),
    display_size=sp.REZ_1080p,
    window_name="example 1 basic code"
)

class Box(sp.Render):
    def __init__(self, x, y, size, color):
        surf = pygame.Surface((size, size))
        surf.fill(color)
        super().__init__(x, y, size, size, angle=0, surf=surf)

box = Box(100, 100, 64, (200, 50, 50))

dm.root_layer.add_obj(box)  # <- this is what makes it show up on screen

dm.START_RENDERING_THREAD(fps=60)
while dm.running:

    dm.event_handler()
    dm.tick(tps=60)

```
in this example there is a class which inherits from the Render class so it can be rendered through skyport-engines rendering pipeline

## example 2 layer basics
```python
ui_layer = sp.Layer(width=800, height=600)
ui_menue_layer = sp.Layer(width=100,height=600)

ui_layer.add_obj(ui_menue_layer) # you can put a Layer inside of a Layer because a Layer inherits from the Render class

world_layer = sp.Layer(width=800, height=600)

world_layer.add_obj(box)
ui_layer.add_obj(Box(10, 10, 32, (255, 255, 0)))  # (a health icon)

# to get a layer rendered you need to add it to the root_layer
dm.root_layer.add_obj(world_layer)
# layers added after previous layers will be on top of the layer added before
dm.root_layer.add_obj(ui_layer)
```

## more will come :
more examples will come in later updates

# Content :

## Display_Manager:
The Display_Manager is made to automatically handle the window on a separate thread so that the rendering loop can be separate from the game loop.
Note :
```
the display manager has been mostly covered in the getting started section but here we will dive a bit deeper into what the Display_Manager actually can do
```

there is an experimental feature that allows you to add a function that is called after rendering every frame
```python
# this function will be called every frame after rendering everything
display_manager.pos_render_hook = lambda self : ......
```

## Loader:
The Loader is made to pre-cache files in memory.
 
### How to make a Loader map:
```json
{
    "path":{"value":null,"type":null},
    ...
}
```
In the Loader_map format there are a couple of types, and each does different things.
 
TYPES:
- "r" / "replace": this type will read the file from where "value" says to, rather than the original path. Example: ```json "path/jeff.png":{"value":"replacement_path.jpg","type":"r"} ```
- "list": this mode allows you to load an entire file under one name by having "path" as the folder dir and then in "value" you would place a list of files you would like to load. Example: ```json "path/dave":{"value":["bob.png","sam/frend.json"...],"type":"list"} ``` How you would access this is: loader_instance.read(path)[index]
- "dict": this type allows you to load an entire folder under one name, slightly differently. The "path" would be the folder dir just like in "list", but this time "value" is filled with a dict of {"name":"file"} instead of a list. Example: ```json "assets/rand":{"value":{"dave":"dave.png","bob":"idk/jeff.csv"},"type":"dict"} ``` How you would access it is by: loader_instance.read(path)[name]
- null: this is the default, and it doesn't matter what the "type" is. This mode will load the "path" into the Loader map, and how you would access the file would be: loader_instance.read(path)
FULL FILE EXAMPLE:
```json
{
    "assets/jeff.png":{
        "value":null,
        "type":null
    },
    "assets/rand/":{
        "value":[
            "ball.json",
            "ball.png"
        ],
    "type":"list"
    },
    "assets/icons": {
        "type": "dict",
        "value": {
            "button1": "button_play.png",
            "coin": "coin.png",
            "button2": "button2.png"
        }
    },
    "assets/files/data.png":{
        "value":"texture_pack/file/data.png",
        "type":"replace"
    }
}
```
### How to use Loader in code:
The Loader is simple: you first create an instance and pass in `__file__` so that the Loader can resolve relative paths, then you can preload the Loader with a Loader map (this is optional), then you can use the Loader to read files whether they have been pre-cached or not. Example code:
```python
import skyport as sp
 
# you need to pass in __file__ so the Loader knows how to resolve relative paths
loader = sp.Loader(__file__)
# (optional) you can preload all the files you want into the Loader so you don't have to get them later
loader.load_from_map("loader_map_test1.json")
# to read the content of the file whether the Loader has pre-loaded it or not, you can call read()
file_data = loader.read(path="my_path/image.png",add_to_map=True)
# this method allows you to create more convenient names for your pre-loaded files
loader.create_alias("my_path/image.png","image")
# this returns the dict of supported types and their file handlers
loader.get_supported_types()
# you can add your own file types for the loader to support if the loader doesn't already support them
loader.add_new_file_handler(".idk",lambda path : open(path,"r"))
# this is for when you have two loaders or two name:file maps
loader.join_maps(map_a=,map_b=)
# this is for turning any path into an abs path
loader.resolve_path(path)
# this is for saving files that are in the Loader
loader.save(path,map_key)
# this is for saving the current in-memory file map
loader.save_map(path,map)
# this is for getting the Loader map
loader.get_map()
# this is for setting the Loader map
loader.set_map(map)
```
There are more methods in the loader, but they are not mentioned here.
 
### How to handle supported / unsupported file types:
There are two decorators for adding file type support: Load_file and Save_file.
Load_file takes in a lambda that takes in a file object and loads it, and Save_file takes in a lambda that takes in a file object and the data to save. For example:
```json
    ".txt": Load_file(lambda f: f.read()),
    ".json": Load_file(json.load)
```
And for saving:
```json
    ".jpeg": lambda p, d: pygame.image.save(d, p),
    ".txt": Save_file(lambda f, d: f.write(str(d)))
```
note : Load_file and Save_file are decorators that ensure that when the file is open it is safely closed
 
## Util:
This is a utility class that holds random and potentially useful methods, such as:
- get_angle_and_dist(self,x1:"int",y1:"int",x:"int",y:"int")
- priority(self,a=None,b=None): this function will return `a` if there is an `a`
- snap_cords_in_bounds(self,x:"int",y:"int",max_x:"int",max_y:"int",min_x:"int"=0,min_y:"int"=0): this snaps coordinates inside of a rectangular area
- couculate_dx_dy(self,dist:"int",angle:"float")
- couculate_angle_dist(self,dx:"int",dy:"int")
- play_sound(self,soud_obj:"pygame.mixer.Sound", volume=0.5,loops=0)
- play_sound_from_point(self,pf,sound_pos:list,listener_pos:list,volume:float=0.5,loops=0,distance_fade=0.5)
- warp_image(self,image:"pygame.Surface",sizex:"int",sizey:"int",angle:"float")
- rotate_image(self,image:"pygame.Surface",angle:"float")
- scale_image(self,image:"pygame.Surface",sizex:"int",sizey:"int")
- color_swap(self,surface: 'pygame.Surface', old_color: tuple, new_color: tuple) -> 'pygame.Surface'
How to use the Util class:
```python
import skyport as sp
 
util = sp.Util()
 
a = util.pryoraty(None,2) # a will be 2
 
print(a)
```

## Delta_timer:
This is for getting the delta time between the current call of `get_dt()` and the last one (there is one method -> get_dt()).
```python
dt = sp.Delta_timer()

time_dif = dt.get_dt()

```
get_dt returns the difference in time from when the instance is created to the call of get_dt and then if you call it again it returns the difference in time from the last call to the current call
 
## Render :
the render class is a data class holding data like rect,x,y,angle,surface and has a couple utility tools and it was made to be used like :
```python

class My_Obj(sp.Render):
    def __init__(self, x, y, width, height, angle, surf = None):
        super().__init__(x, y, width, height, angle, surf)
        # my objs data like health or max speed .......
   
    # rest of class .....
```
the Render class would just automatically handle image scaling and rotating
Note: ``` most of the time image updating is automatic when you use set_angle or set_size but if you mod the OG image you will have to call my_class_instance.update_surf() for it to update (this might change in a later update) ```

Note2: ``` x and y are not stored inside of the Render instead they are stored inside Render.rect.x / Render.rect.y ```
### Render binds:
each render can have binds like a button

to add a bind to a Render type obj you go
```python
my_render.add_bind(event=pygame.MOUSEBUTTONDOWN,button=1,func=[func1...])

```
just like binds with the Display_Manager you don't have to pass in a list/tuple of functions you can also just pass in 1 function

these binds are applied only to the Render type obj you apply them to and all functions you pass in need to take in 2 parameters self and event (event is the standard pygame event that you would get from the event handler)

#### how do do a GUI button:
to do a GUI button you will add a Render bind to ur render instance with a function that would detect if the Renders rect is colliding with the mouse
```python
def on_clik(self,event):
    if self.rect.collidepoint(display_manager.mouse_pos):
        print(f"render {self.id} was clicked")

my_render_instance.add_bind(event=pygame.MOUSEBUTTONDOWN,,button=1,funt=on_click)
```
now when the mouse clicks on this Render instance it will print the data

## Layer :
the layer can hold many objs that inherit the from Render class and then it will automatically render them to the display every frame and it will call the update function of the Render class every frame

```python
class my_class(skyport.Render):
    ...

my_layer = skyport.Layer(width=100,height=100)

# you can add layers in layers
my_layer.add_obj(skyport.Layer(...))

# you can also add any class that inherits from skyport.Render class
my_layer.add_obj(my_class)

# you can remove obj from a layer
my_layer.remove_obj(my_class)

# if u know the id of an obj u can get it from a layer
my_layer.get_obj_from_id(1)

# there are more basic methods
```
to use layers and renders to their full potential and render them every frame you just need to put them in your display managers root_layer and then the Renders / Layers will auto update and auto render to the display every frame
```python
my_layer.add_obj(my_obj)

#the obj and the layer will be drawn to the display and window every frame and it will call the update method of my_obj and my_layer every frame as well
display_manager.root_layer.add_obj(my_layer)
```

### methods :
some of the methods as of this update are :
    - get_surf : this returns the up to date surf
    - get_pos : this returns -> (x,y)
    - get_size : this returns -> (width,height)
    - set_angle : this sets the angle and auto updates the surf
    - set_size : this sets the size and auto updates the surf

## Chunked_Layer:
with this u can render massive worlds without hitting a performance wall

### how to use
bc this is just a Render at hart u can use it in any spot where a Render type class can be used including in other Layer's or with in a chunk of a chunk layer
```python
world = sp.Chunked_Layer(
    x=0,
    y=0,
    width=DISPLAY_SIZE[0],
    height=DISPLAY_SIZE[1],
    angle=0, # bc this is a render u can angle this
    chunk_size=CHUNK_SIZE,
    chunk_genorator=chunk_genorator, # this will be used to genorate each chunk and it will be passed in the chunks self
    chunk_updateor=chunk_updateor, # this function will be passed to each chunk on generation and the chunk will call it every time the chunk is updated
)
```
other than a few things this is just a base Render

to move the camera around u will use chunk_layer.camera_x , chunk_layer.camera_y
```python
world.camera_x += 5

world.camera_y += 2
```
this will move what part of the world that will be rendered to the chunked layer .image surface

do add an Render type class you would use your chunk layers chunk_obj method which would figure out what chunk the obj belongs to then it would add the obj to that chunk
```python
class myclass(Render):
    ...

my_obj = myclass(parameters)

# this would put it into the chunk that the objs cords would be in
chunk_layer.chunk_obj(my_obj)

```
if you have moving objects you might want them to not un load when the chunk layer stops updating there chunks and to do that you would want to occasionally re chunk a chunk
```python
# this would re chunk all objs in this chunk to there corect chunk
chunk_layer.re_chunk(cx=1,cy=2)
```
you can also re chunk all loaded chunks
```python
chunk_layer.rechunk_all_chunks()
```
there is also a way to rechunk only loaded chunk
```python
chunk_layer.re_chunk_visable()
```

chunk_layers do have a couple of useful tools
```python
# this returns the chunk at the pos and if it does not exist it will generate it
chunk = chunk_layer.get_tile(cx=1,cy=5)

# this will return the chunk at the chunk cords only if it already exists
chunk = chunk_layer.get_existing_tile(cx=1,cy=6)

#this will remove the chunk if it exists
chunk_layer.remove_tile(cx,cy)

# this will tell you what chunk that these cords would end up in
chunk_pos = chunk_layer.pos_to_cpos(x=100,y=224)

# this will tell u the pos of the chunk
pos = chunk_layer.cpos_to_pos(cx=1,cy=2)


```

## Chunk:
the Chunk is just a modified Layer modded to be used with the Chunk_Layer class to be rendered and render other Render type class instances
Note :
```md
the chunk is not meant to be directly used
```
## Camera :
the Camera class is like the Chunked_layer class but it is for smaller worlds but the Camera class adds a zoom mechanic so you can zoom in and out of your worlds
```python

cam = sp.Camera(width=100,height=100,world_width=300,world_height=300)

cam.set_cam_pos(30,30)

cam.zoom = 1.2

```
the Camera class inherits from the layer class so it has all the same methods and attars 

Note:
``` the cam.zoom is a size multiplier so a zoom of 2 makes the map zoom in 2x more and a zoom of 0.8 make the map zoom to 0.8x the OG size ```

more documantation will come

## Font_Render:
this is a render that renders text
so you can use it just like a normal Render but it auto renders text
```python
font = pygame.font.SysFont("arial", 22)

my_text = sp.Font_Render(
    font=font,
    text="my msg",
    x=350, y=20, width=400, height=40,
    text_color=(255, 255, 0),
    word_wrapping=False,
    bg_color=(100,100,100)
)
```
if word wrapping is True then the text will wrap to stay with in the rect of the Render

## SDL2_Display_Manager :
Note:
    this is experimental and might not work on your OS and some features might be broken
this is the same as Display_Manager but it uses pygames sdl2 to use your gpu (more documentation will come ...)
Note :
```md
do not call the event handler when using the sdl2 display manager (it might freeze or crash)
```

## decorators :
skyport-engine has a few built in decorators which might be useful such as
- Button(get_mouse_pos_func)
- Once()
- Save_file
- Load_file

each decorator should say in the IDE a bit about it but in case you don't have an IDE which does that here is a bit of data on each of the decorators

Note: the only 2 decorators that are not in skyport.decorators is Load_file and Save_file because they are part of the Loader

### Button :
this decorator is meant to be used with Render type classes (classes which inherit from the Render class or just the Render class)

if you decorate your function with this decorator your function will only execute when the mouse is over the rect of the Render type class but because this decorator is built for the Render class your function has to accept two parameters self (the class instance) and event (the pygame event that triggered the call of your function)
```python
@skyport.decorators.Button(display_manager.get_mouse_pos)
def my_func(self,event):
    ...
```
Note : make sure that you pass in the get_mouse_pos function into this decorator
### Once :
this decorator will allow your function to be called once per instance
``` python
@skyport.decorators.Once
def my_func():
    ...
# now this function can only be called once per class instance
```
## audio engine 
an audio engine will be coming to skyport in later updates


# dev_info:
email: matthew.le.robins+proj@gmail.com

    (please only email if you have found a bug and a way to fix it or have a suggestion , and please ensure I have a way to contact you about your suggested changes or requests)
