diff --git a/README.md b/README.md index 34fc48d..56f2ba8 100644 --- a/README.md +++ b/README.md @@ -1,35 +1,79 @@ -# Silly_Ninja +# SILLY NINJA - MULTIPLAYER CO-OP GAME ## DESCRIPTION -A multiplayer co-op game made with __Pygame__ and __Socket__, assets credit goes to _DaFluffyPotato_. +You're a ninja on a land full of bad guys, but even worse, they're armed with guns. You have nothing but your own ninja spirit, can that be a weapon to deal with them? Or perharps, bring over some of your fellow ninjas could be a good option to consider. -## PYTHON EXTERNAL MODULES NEEDED +A _multiplayer_, _co-op_ game made with __Pygame__, networking was made possible with __Socket__, assets credit goes to _DaFluffyPotato_. + +## CONTROLS +### Game: +- A, Left Arrow: Move to the left +- D, Right Arrow: Move to the right +- Space, Up Arrow: Jump +- L.Shift, R.Shift: Dash/Attack +- ESC: Back out +### Map Editor: +- W, A, S, D or Arrow Keys: Pan the camera around +- Left Click: Place tiles at the mouse position +- Right Click: Delete tiles at the mouse position +- G: Toggle snap to grid +- Scroll Wheel: Circle tile groups +- Shift + Scroll Wheel: Circle tile variant within current group +- Ctrl + S: Save current map +- Ctrl + R: Auto-format placed ruled tiles +- ESC: Exit edit mode + +## REQUIRED EXTERNAL MODULES +Install modules by the command `python -m pip install [module_name]` or `python3 -m pip install [module_name]`. - pygame -- requests - PyInstaller - pyperclip - gtk (Linux only) - PyQt4 (Linux only) -## Installation -Run the following commands: -- git clone https://github.com/constance012/Silly_Ninja.git -- cd Silly_Ninja/‘Silly Ninja’ -- Windows: -python main_menu.py -- Linux: -python3 main_menu.py +## INSTALLATION +### From Source +- Clone the repo with `git clone https://github.com/constance012/Silly_Ninja.git`. +- Install all required modules from the above section. +- Navigate to `Silly Ninja\assets\fonts` and install all required fonts. +- Run these following commands: +``` +cd Silly_Ninja/‘Silly Ninja’ +For Windows: python silly_ninja.py +For Linux: python3 silly_ninja.py +``` +### From Executables (Windows only for now) +- Download the [___lastest release___](https://github.com/constance012/Silly_Ninja/releases). +- Extract the content. +- Navigate to the `fonts` folder and install all required fonts. +- If you want to open the map editor, go to `executables/map_editor_win_x64` and run `map_editor.exe`. +- For the game, go to `executables/silly_ninja_win_x64` and run `silly_ninja.exe`. + +## MULTIPLAYER INSTRUCTION +Because the game only supports multiplayer through Local Area Network (LAN), there're couple of ways to establish connections and play with your friends: +- You and your friends must be on the same network or Wifi, so that the host's IP can be discovered by other clients. +- Using third party software that provide the ability to create your own virtual networks, such as _Hamachi_, _RadminVPN_ or _ZeroTier_,... just to name a few. Then you and your friend can join the same network and establish connection. This is actually the prefer method because y'all can connect to each other from anywhere on the globe, as long as your device remain in that said virtual network. + +After that, open the game and press the `Join` or `Host` button, depends on your situation: +- For the host, enter local IP on your network to the `IP` field, and enter a port number to the `Port` field (must be greater than 1000). After that, choose a nickname and press `Start`, you'll be in the lobby if the server starts successfully. +- For the client, enter the Host's IP and port to both fields. After that, pick a nickname and press `Join`, you'll be in the lobby if the connection establishes successfully. +- After all clients have joined the lobby and ready, indicates by their slots borders turn green, the Host then can start the game by pressing the `Launch` button. + +## KNOWN ISSUES +- Levels are currently be order by ID as an integer. So when you create a new level using the Map Editor, its ID must be an integer that goes after the last level in the `assets/maps` folder, otherwise the game will crashes on level transitions. +- Levels are __NOT__ synced between machines on multiplayer mode, so if you make a new level or delete an existing one using the Map Editor. Then those new changes won't be shared across multiple devices in multiplayer mode, resulting in weird behaviors or even crashes during runtime. This issue has been acknowledged by us and will be fixed on future update. The current workaround is to have the host send his level files to all the clients before hosting a session. -## Note -- Before running main_menu.py, you must navigate to the 'fonts' folder to install all the fonts contained within it. -- Ensure that all required libraries are installed to run the game. -- For multiplayer gameplay, you need to install ZeroTier to establish connections. Here is the documentation for your reference on how to download and use zerotier. [link](https://docs.zerotier.com/start) +## NOTES +- Ensure that all required libraries, modules are installed if you want to compile and run the game directly from source. +- For executables, only the fonts are required. -## Contributions -@Hikiyoshi - develop basegame +## CREDITS +Special thanks to [___DaFluffyPotato___](https://www.youtube.com/@DaFluffyPotato) for the gorgeous image assets and audio. -@constance012 - develop networking +## CONTRIBUTIONS +@Hikiyoshi - Programmer, Tester, Level Design. +@constance012 - Programmer, Networking, UI Design. -## In game Captures -![Main Menu](https://private-user-images.githubusercontent.com/96867270/331322071-fd2297b5-96d2-4295-a5b2-c379cd95bbae.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTU5Mjg1ODQsIm5iZiI6MTcxNTkyODI4NCwicGF0aCI6Ii85Njg2NzI3MC8zMzEzMjIwNzEtZmQyMjk3YjUtOTZkMi00Mjk1LWE1YjItYzM3OWNkOTViYmFlLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA1MTclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNTE3VDA2NDQ0NFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTQwMTEzMzdkMDNmYTZjY2RlZWFiMDcwZmJmOGFiNzI0Mjk3MjFhN2EzZGIzMzI3NjM5MzllMmZiOTJiNTA0MzImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.SO71XJMyowEFEOrW6aiBVu96MoTp70aUoU63SA7zrPI) +## IN GAME CAPTURES +![Main Menu](https://private-user-images.githubusercontent.com/96867270/331322071-fd2297b5-96d2-4295-a5b2-c379cd95bbae.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTU5Mjg1ODQsIm5iZiI6MTcxNTkyODI4NCwicGF0aCI6Ii85Njg2NzI3MC8zMzEzMjIwNzEtZmQyMjk3YjUtOTZkMi00Mjk1LWE1YjItYzM3OWNkOTViYmFlLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA1MTclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNTE3VDA2NDQ0NFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTQwMTEzMzdkMDNmYTZjY2RlZWFiMDcwZmJmOGFiNzI0Mjk3MjFhN2EzZGIzMzI3NjM5MzllMmZiOTJiNTA0MzImWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.SO71XJMyowEFEOrW6aiBVu96MoTp70aUoU63SA7zrPI) ![In Game](https://private-user-images.githubusercontent.com/96867270/331322655-547c06de-fefa-4a28-b791-44b873f484e3.png?jwt=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJnaXRodWIuY29tIiwiYXVkIjoicmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbSIsImtleSI6ImtleTUiLCJleHAiOjE3MTU5Mjg1ODQsIm5iZiI6MTcxNTkyODI4NCwicGF0aCI6Ii85Njg2NzI3MC8zMzEzMjI2NTUtNTQ3YzA2ZGUtZmVmYS00YTI4LWI3OTEtNDRiODczZjQ4NGUzLnBuZz9YLUFtei1BbGdvcml0aG09QVdTNC1ITUFDLVNIQTI1NiZYLUFtei1DcmVkZW50aWFsPUFLSUFWQ09EWUxTQTUzUFFLNFpBJTJGMjAyNDA1MTclMkZ1cy1lYXN0LTElMkZzMyUyRmF3czRfcmVxdWVzdCZYLUFtei1EYXRlPTIwMjQwNTE3VDA2NDQ0NFomWC1BbXotRXhwaXJlcz0zMDAmWC1BbXotU2lnbmF0dXJlPTU4OWYwOTkyNDQ5NDM0MGFhNmU1NTdmYWZhYjM4ZGU0NmY3ZWVjODMxOTc0OWFkMzIyMmMzMWE2ZTNlOWRiMTkmWC1BbXotU2lnbmVkSGVhZGVycz1ob3N0JmFjdG9yX2lkPTAma2V5X2lkPTAmcmVwb19pZD0wIn0.eo7fj2nDd5W88Df_wnM734AMFKJRahQi6j-9Z07iQYs) diff --git a/Silly Ninja/map_editor.py b/Silly Ninja/map_editor.py index d3c4835..cb87309 100644 --- a/Silly Ninja/map_editor.py +++ b/Silly Ninja/map_editor.py @@ -110,8 +110,7 @@ def delete_map(self): def handle_events(self, event): super().handle_events(event) - if event.type == pygame.KEYDOWN: - self.map_id_field.handle_key_pressed(event) + self.map_id_field.handle_key_pressed(event) class MapEditor: @@ -257,16 +256,16 @@ def run(self): if event.key == pygame.K_ESCAPE: running = False fade_out((self.display.get_width(), self.display.get_height()), self.display) - if event.key == pygame.K_a: + if event.key == pygame.K_a or event.key == pygame.K_LEFT: self.movement[0] = True - if event.key == pygame.K_d: + if event.key == pygame.K_d or event.key == pygame.K_RIGHT: self.movement[1] = True - if event.key == pygame.K_w: + if event.key == pygame.K_w or event.key == pygame.K_UP: self.movement[2] = True - if event.key == pygame.K_s: + if event.key == pygame.K_s or event.key == pygame.K_DOWN: self.movement[3] = True print(MAP_ID) - if self.control_held: + if self.control_held and event.key == pygame.K_s: self.tilemap.save(f"assets/maps/{MAP_ID}.json") if self.control_held and event.key == pygame.K_r: self.tilemap.ruletile() @@ -278,13 +277,13 @@ def run(self): self.control_held = True if event.type == pygame.KEYUP: - if event.key == pygame.K_a: + if event.key == pygame.K_a or event.key == pygame.K_LEFT: self.movement[0] = False - if event.key == pygame.K_d: + if event.key == pygame.K_d or event.key == pygame.K_RIGHT: self.movement[1] = False - if event.key == pygame.K_w: + if event.key == pygame.K_w or event.key == pygame.K_UP: self.movement[2] = False - if event.key == pygame.K_s: + if event.key == pygame.K_s or event.key == pygame.K_DOWN: self.movement[3] = False if event.key == pygame.K_LSHIFT or event.key == pygame.K_RSHIFT: self.shift_held = False diff --git a/Silly Ninja/scripts/game.py b/Silly Ninja/scripts/game.py index 9cf5dcf..cc3e778 100644 --- a/Silly Ninja/scripts/game.py +++ b/Silly Ninja/scripts/game.py @@ -577,7 +577,7 @@ def run(self): class GameSolo(GameBase): def __init__(self, clock, screen, outline_display, normal_display): super().__init__(clock, screen, outline_display, normal_display) - self.player = Player("You", self, (50, 50), (8, 15)) + self.player = Player("", self, (50, 50), (8, 15)) self.start_game() @@ -597,6 +597,7 @@ def load_level(self, id): def run(self): super().run() + self.start_game() while self.running: self.outline_display.fill((0, 0, 0, 0)) self.normal_display.blit(self.assets["background"], (0, 0)) diff --git a/Silly Ninja/scripts/socket/server.py b/Silly Ninja/scripts/socket/server.py index b8a96ef..599f4a3 100644 --- a/Silly Ninja/scripts/socket/server.py +++ b/Silly Ninja/scripts/socket/server.py @@ -1,6 +1,5 @@ import threading import socket -import requests import time from datetime import datetime @@ -46,18 +45,6 @@ def get_nickname_at(self, index): return self.nicknames[index] - def get_public_ip(self): - try: - response = requests.get("https://httpbin.org/ip") - if response.status_code == 200: - response_json = response.json() - return response_json["origin"] - else: - print(f"Failed to retrieve IP - Status Code: {response.status_code}") - except Exception as e: - print(f"An Error occurs: {e}") - - def broadcast(self, message): for client in self.clients: client.send(message.encode(FORMAT)) diff --git a/Silly Ninja/scripts/ui/sub_menus.py b/Silly Ninja/scripts/ui/sub_menus.py index 932af9d..9481531 100644 --- a/Silly Ninja/scripts/ui/sub_menus.py +++ b/Silly Ninja/scripts/ui/sub_menus.py @@ -72,7 +72,20 @@ def back_out(self): self.running = False -class HostMenu(MenuBase): +class SubMenuBase(MenuBase): + def __init__(self): + super().__init__() + self.back_button = Button("Back", "gamer", (220, 390), (150, 60), on_click=self.back_out) + + + def handle_events(self, event): + super().handle_events(event) + if event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: + self.back_button.click(MenuBase.screen, self.fade_alpha) + + +class HostMenu(SubMenuBase): def __init__(self): super().__init__() self.default_ip = socket.gethostbyname(socket.gethostname()) @@ -96,7 +109,6 @@ def __init__(self): self.status_text = Text("", "retro gaming", (CENTER, 360), size=13, color=pygame.Color("crimson")) - self.back_button = Button("Back", "gamer", (220, 390), (150, 60), on_click=self.back_out) self.start_button = Button("Start", "gamer", (420, 390), (150, 60), on_click=self.start_hosting, fade_out=False) @@ -207,7 +219,7 @@ def back_out(self): self.status_text.set_text("") -class JoinMenu(MenuBase): +class JoinMenu(SubMenuBase): def __init__(self): super().__init__() self.default_port = 5050 @@ -229,7 +241,6 @@ def __init__(self): self.status_text = Text("", "retro gaming", (CENTER, 360), size=13, color=pygame.Color("crimson")) - self.back_button = Button("Back", "gamer", (220, 390), (150, 60), on_click=self.back_out) self.join_button = Button("Join", "gamer", (420, 390), (150, 60), on_click=self.try_joining, fade_out=False) @@ -338,7 +349,7 @@ def back_out(self): self.status_text.set_text("") -class Lobby(MenuBase): +class Lobby(SubMenuBase): def __init__(self, game_instance, server=None, is_host=False): super().__init__() @@ -377,10 +388,10 @@ def __init__(self, game_instance, server=None, is_host=False): self.status_text = Text("", "retro gaming", (CENTER, 365), size=13, color=pygame.Color("crimson")) if self.is_host: - self.back_button = Button("Back", "gamer", (220, 390), (150, 60), on_click=self.back_out) self.launch_button = Button("Launch", "gamer", (420, 390), (150, 60), on_click=self.launch, fade_out=False) else: - self.back_button = Button("Back", "gamer", (CENTER, 390), (150, 60), on_click=self.back_out) + self.back_button.pos = (CENTER, 390) + self.back_button.display_text.pos = (CENTER, 390) def run(self): diff --git a/Silly Ninja/scripts/ui/ui_elements.py b/Silly Ninja/scripts/ui/ui_elements.py index f3f39bd..d765980 100644 --- a/Silly Ninja/scripts/ui/ui_elements.py +++ b/Silly Ninja/scripts/ui/ui_elements.py @@ -137,6 +137,16 @@ def reset_state(self): self.interactable = True + def click(self, surface, fade_alpha): + if self.fade_out: + fade_out((surface.get_width(), surface.get_height()), surface, color=BLACK) + fade_alpha = 255 + + self.reset_state() + self.on_click(*self.args) + return fade_alpha + + def update(self, surface, fade_alpha, mx, my, click): if self.interactable: if self.rect.collidepoint((mx, my)): @@ -144,12 +154,7 @@ def update(self, surface, fade_alpha, mx, my, click): self.rect.w = min(self.width + 20, self.rect.w + self.expand_speed) if click and self.on_click is not None: - if self.fade_out: - fade_out((surface.get_width(), surface.get_height()), surface, color=BLACK) - fade_alpha = 255 - - self.reset_state() - self.on_click(*self.args) + fade_alpha = self.click(surface, fade_alpha) elif self.rect.w != self.width: self.text_color = WHITE diff --git a/Silly Ninja/silly_ninja.exe b/Silly Ninja/silly_ninja.exe deleted file mode 100644 index df493f8..0000000 Binary files a/Silly Ninja/silly_ninja.exe and /dev/null differ diff --git a/Silly Ninja/main_menu.py b/Silly Ninja/silly_ninja.py similarity index 96% rename from Silly Ninja/main_menu.py rename to Silly Ninja/silly_ninja.py index 90a3af5..6398aca 100644 --- a/Silly Ninja/main_menu.py +++ b/Silly Ninja/silly_ninja.py @@ -25,7 +25,7 @@ def __init__(self): # UI Elements. self.title = BorderedText("SILLY NINJA", "retro gaming", (CENTER, 30), size=70, bold=True) - self.version_text = Text("----- Beta v0.9 -----", "retro computer", (CENTER, 130), size=15) + self.version_text = Text("----- Released v1.0 -----", "retro computer", (CENTER, 130), size=15) self.solo_button = Button("Solo", "gamer", (CENTER, 180), (150, 60), on_click=self.game_solo.run) self.join_button = Button("Join", "gamer", (CENTER, 250), (150, 60), on_click=self.join_menu.run)