diff --git a/MARWIN_ADVENTURE.py b/MARWIN_ADVENTURE.py new file mode 100644 index 0000000..aa94dd7 --- /dev/null +++ b/MARWIN_ADVENTURE.py @@ -0,0 +1,147 @@ +from button import Button +import pygame +import sys +import settings as config +pygame.init() + +# Screen state flags +is_darkening = False # Start darkening animation +is_finished_darkening = False # Darkening animation finished +overlay_alpha = 0 # Darkness value + +overlay_surface = pygame.Surface((config.XWIN, config.YWIN)) # Create surface for dimming +window = pygame.display.set_mode(config.DISPLAY, config.FLAGS) # Screen +pygame.display.set_caption("MARWIN ADVENTURE") # Set title +pygame.display.set_icon(pygame.image.load(config.icon)) # Set icon + +BG = pygame.image.load(config.main_menu_background) # Background + +# Button images +play_button_image = pygame.image.load(config.start_button_non_active) # START +exit_button_image = pygame.image.load(config.exit_button_non_active) # EXIT + +# Sound flags +play_button_PLAY_sound = False # Play button sound activation +play_button_EXIT_sound = False # Exit button sound activation + +# Darkening animation +def darken_screen(): + global overlay_alpha, is_darkening, is_finished_darkening # Global variables + overlay_alpha += 5 # Increase darkness value + if overlay_alpha >= 255: + # Animation completion + is_darkening = False + is_finished_darkening = True + overlay_surface.set_alpha(overlay_alpha) # Set darkness + if config.music_volume > 0.0: + config.music_volume -= 0.001 + pygame.mixer.music.set_volume(config.music_volume) # Apply reduced volume + window.blit(overlay_surface, (0, 0)) # Draw + +# Button action functions +def start_game(): # Start game for START button + global is_darkening + is_darkening = True + play_button.clicked = True + darken_screen() + +def exit_game(): # Exit game for EXIT button + pygame.quit() + sys.exit() + +# Text display +def display_text(text, font_size, color, font_path, pos): + font = pygame.font.Font(font_path, font_size) + text_render = font.render(text, True, color) + text_rect = text_render.get_rect(center=pos) + window.blit(text_render, text_rect) + +# Button instances +play_button = Button(play_button_image, (config.XWIN // 2, config.YWIN // 2), action=start_game) +exit_button = Button(exit_button_image, (config.XWIN // 2, config.YWIN // 1.35), action=exit_game) + +def Run(): + global overlay_alpha, is_darkening, is_finished_darkening, play_button_PLAY_sound, play_button_EXIT_sound + # Reset before loop start + config.music_volume = 0.2 + is_finished_darkening = False + is_darkening = False + overlay_alpha = 0 + + # Load animation background + bg_y = 0 # Background coordinates + bg_speed = 1 # Background scroll speed + + # Start music + pygame.mixer.music.load(config.overworld_music) + pygame.mixer.music.set_volume(config.music_volume) + pygame.mixer.music.play(-1) + + while True: + mouse_pos = pygame.mouse.get_pos() # Handle mouse position + for event in pygame.event.get(): + if event.type == pygame.QUIT: + pygame.quit() + sys.exit() + elif event.type == pygame.MOUSEBUTTONDOWN: + # Button function activation on click + play_button.check_click(mouse_pos) + exit_button.check_click(mouse_pos) + + # If darkening animation finished, launch main.py + if is_finished_darkening: + pygame.mixer.music.stop() + import main # Import + main.run_game() # Initialization + # Reset parameters after launch + is_finished_darkening = False + is_darkening = False + overlay_alpha = 0 + + # Display background and buttons + window.blit(BG, (0, bg_y)) + window.blit(BG, (0, bg_y - config.YWIN)) # Extra display for covering image boundary + + # Update background position for vertical scrolling + bg_y += bg_speed + if bg_y >= config.YWIN: + bg_y = 0 + + # Display buttons and handle their hover reactions + if not is_darkening or not is_finished_darkening: # If not in darkening animation + # Play button + if play_button.rect.collidepoint(mouse_pos): + if play_button_PLAY_sound: + config.check_button.play() + play_button_PLAY_sound = False + window.blit(pygame.image.load(config.start_button_active), play_button.rect) + else: + play_button_PLAY_sound = True + window.blit(play_button.image, play_button.rect) + + # Exit button + if exit_button.rect.collidepoint(mouse_pos): + if play_button_EXIT_sound: + config.check_button.play() + play_button_EXIT_sound = False + window.blit(pygame.image.load(config.exit_button_active), exit_button.rect) + else: + play_button_EXIT_sound = True + window.blit(exit_button.image, exit_button.rect) + + # Display MARWIN image + display_text("MARWIN", 80, config.DARK_BLUE, config.pixel_font, (config.XWIN // 2, config.YWIN // 6)) + display_text("ADVENTURE", 80, config.DARK_BLUE, config.pixel_font, (config.XWIN // 2, config.YWIN // 3.5)) + + if is_darkening: # Screen darkening at game start + darken_screen() + + pygame.display.update() # Update screen + pygame.time.Clock().tick(config.FPS) # Set FPS + +def main(): # Function to initialize launch, allowing to return to the main menu while in the game + Run() + +if __name__ == "__main__": + # Program start + main() diff --git a/README.md b/README.md index 57c610a..c142c90 100644 --- a/README.md +++ b/README.md @@ -1,17 +1,17 @@ -# Pygame DoodleJump -The simplest doodle jump made with python3 and pygame in only 2 days +# Pygame DoodleJump (MARWIN ADVENTURE) +### The game like a doodle jump game based on original code with using a pygame ## Table of contents -* [General info](#general-info) -* [Requirements](#requirements) -* [Setup](#setup) -![Screenshot](https://github.com/MykleCode/pygame-doodlejump/blob/main/demo.gif) + +Screenshot Screenshot Screenshot Screenshot + ## General Info -* No images used for graphics +* Work has been done to improve the visual * Well clean and organised code -* Relatively small code +* Code is optimized and performant +* Every part of the code is uncommented ## Requirements * [Python3](https://www.python.org/downloads/) @@ -21,3 +21,4 @@ The simplest doodle jump made with python3 and pygame in only 2 days * Download zip, fork or clone. * Install requirements * And Run ! +* If you have any problems contact me ! diff --git a/audio/effects/activate_button.ogg b/audio/effects/activate_button.ogg new file mode 100644 index 0000000..ac49cee Binary files /dev/null and b/audio/effects/activate_button.ogg differ diff --git a/audio/effects/check_button.ogg b/audio/effects/check_button.ogg new file mode 100644 index 0000000..95d61bd Binary files /dev/null and b/audio/effects/check_button.ogg differ diff --git a/audio/effects/explosion.ogg b/audio/effects/explosion.ogg new file mode 100644 index 0000000..299d126 Binary files /dev/null and b/audio/effects/explosion.ogg differ diff --git a/audio/effects/game_over.ogg b/audio/effects/game_over.ogg new file mode 100644 index 0000000..7408419 Binary files /dev/null and b/audio/effects/game_over.ogg differ diff --git a/audio/effects/jump.ogg b/audio/effects/jump.ogg new file mode 100644 index 0000000..c826a1e Binary files /dev/null and b/audio/effects/jump.ogg differ diff --git a/audio/effects/stomp.ogg b/audio/effects/stomp.ogg new file mode 100644 index 0000000..e902a11 Binary files /dev/null and b/audio/effects/stomp.ogg differ diff --git a/audio/level_music.wav b/audio/level_music.wav new file mode 100644 index 0000000..72856fc Binary files /dev/null and b/audio/level_music.wav differ diff --git a/audio/overworld_music.wav b/audio/overworld_music.wav new file mode 100644 index 0000000..6fe7d74 Binary files /dev/null and b/audio/overworld_music.wav differ diff --git a/button.py b/button.py new file mode 100644 index 0000000..94a872b --- /dev/null +++ b/button.py @@ -0,0 +1,14 @@ +import pygame + +# Class for management buttons in interface +class Button(pygame.sprite.Sprite): + def __init__(self, image, pos, action=None): + super().__init__() + self.image = image + self.rect = self.image.get_rect(center=pos) + self.action = action # Triggered function + + # Run function on mouse click + def check_click(self, mouse_pos): + if self.rect.collidepoint(mouse_pos): # If there is collision + self.action() # Launching \ No newline at end of file diff --git a/camera.py b/camera.py index f57045e..a202a47 100644 --- a/camera.py +++ b/camera.py @@ -1,73 +1,37 @@ -# -*- coding: utf-8 -*- -""" - CopyLeft 2021 Michael Rouves - - This file is part of Pygame-DoodleJump. - Pygame-DoodleJump is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Pygame-DoodleJump is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with Pygame-DoodleJump. If not, see . -""" - - -from pygame import Rect -from pygame.sprite import Sprite - +import pygame from singleton import Singleton import settings as config - - class Camera(Singleton): - """ - A class to represent the camera. - - Manages level position scrolling. - Can be access via Singleton: Camera.instance. - (Check Singleton design pattern for more info) - """ - # constructor called on new instance: Camera() - def __init__(self, lerp=5,width=config.XWIN, height=config.YWIN): - self.state = Rect(0, 0, width, height) - self.lerp = lerp - self.center = height//2 - self.maxheight = self.center - - def reset(self) -> None: - " Called only when game restarts (after player death)." - self.state.y = 0 - self.maxheight = self.center - - def apply_rect(self,rect:Rect) -> Rect: - """ Transforms given rect relative to camera position. - :param rect pygame.Rect: the rect to transform - """ - return rect.move((0,-self.state.topleft[1])) - - def apply(self, target:Sprite) -> Rect: - """ Returns new target render position based on current camera position. - :param target Sprite: a sprite that wants to get its render position. - """ - return self.apply_rect(target.rect) - - def update(self, target:Rect) -> None: - """ Scrolls up to maxheight reached by player. - Should be called each frame. - :param target pygame.Rect: the target position to follow. - """ - # updating maxheight - if(target.y pygame.Rect: + # Take a rect as input and return a new rect moved by the negative y-coordinate of the top-left point + return rect.move((0, -self.state.topleft[1])) + + # Return a new target rendering position based on the current camera position + def apply(self, target: pygame.sprite.Sprite) -> pygame.Rect: + # Take a rect as input and a Sprite (a sprite that wants to get a rendering position) + # Return the required rect obtained from the apply_rect function, representing the rendering position for the target sprite + return self.apply_rect(target.rect) + + # Method to move up to the maximum height achieved by the player + def update(self, target: pygame.Rect): + # Update the maximum height + if target.y < self.maxheight: # If the player reaches a new height on the screen + self.lastheight = self.maxheight + self.maxheight = target.y + # Calculate the required camera follow speed + speed = ((self.state.y + self.center) - self.maxheight) / self.lerp + self.state.y -= speed diff --git a/demo.gif b/demo.gif deleted file mode 100644 index 4e3d418..0000000 Binary files a/demo.gif and /dev/null differ diff --git a/graphics/UI/frame.png b/graphics/UI/frame.png new file mode 100644 index 0000000..370f152 Binary files /dev/null and b/graphics/UI/frame.png differ diff --git a/graphics/UI/m.png b/graphics/UI/m.png new file mode 100644 index 0000000..524de5f Binary files /dev/null and b/graphics/UI/m.png differ diff --git a/graphics/background/background.png b/graphics/background/background.png new file mode 100644 index 0000000..534e900 Binary files /dev/null and b/graphics/background/background.png differ diff --git a/graphics/bonus/defaultsituation/power_keg_with_m.png b/graphics/bonus/defaultsituation/power_keg_with_m.png new file mode 100644 index 0000000..11c002d Binary files /dev/null and b/graphics/bonus/defaultsituation/power_keg_with_m.png differ diff --git a/graphics/bonus/explosion/1.png b/graphics/bonus/explosion/1.png new file mode 100644 index 0000000..c30ea12 Binary files /dev/null and b/graphics/bonus/explosion/1.png differ diff --git a/graphics/bonus/explosion/2.png b/graphics/bonus/explosion/2.png new file mode 100644 index 0000000..e5f40ed Binary files /dev/null and b/graphics/bonus/explosion/2.png differ diff --git a/graphics/bonus/explosion/3.png b/graphics/bonus/explosion/3.png new file mode 100644 index 0000000..c1924c8 Binary files /dev/null and b/graphics/bonus/explosion/3.png differ diff --git a/graphics/bonus/explosion/4.png b/graphics/bonus/explosion/4.png new file mode 100644 index 0000000..2a34561 Binary files /dev/null and b/graphics/bonus/explosion/4.png differ diff --git a/graphics/bonus/explosion/5.png b/graphics/bonus/explosion/5.png new file mode 100644 index 0000000..9a1dfd3 Binary files /dev/null and b/graphics/bonus/explosion/5.png differ diff --git a/graphics/bonus/explosion/6.png b/graphics/bonus/explosion/6.png new file mode 100644 index 0000000..d16dbc2 Binary files /dev/null and b/graphics/bonus/explosion/6.png differ diff --git a/graphics/character/fall/2.png b/graphics/character/fall/2.png new file mode 100644 index 0000000..ff76c31 Binary files /dev/null and b/graphics/character/fall/2.png differ diff --git a/graphics/character/fall/3.png b/graphics/character/fall/3.png new file mode 100644 index 0000000..35ca0ae Binary files /dev/null and b/graphics/character/fall/3.png differ diff --git a/graphics/character/fall/4.png b/graphics/character/fall/4.png new file mode 100644 index 0000000..4be3134 Binary files /dev/null and b/graphics/character/fall/4.png differ diff --git a/graphics/character/fall/5.png b/graphics/character/fall/5.png new file mode 100644 index 0000000..c3a64e9 Binary files /dev/null and b/graphics/character/fall/5.png differ diff --git a/graphics/character/jump/1.png b/graphics/character/jump/1.png new file mode 100644 index 0000000..015b27a Binary files /dev/null and b/graphics/character/jump/1.png differ diff --git a/graphics/character/jump/2.png b/graphics/character/jump/2.png new file mode 100644 index 0000000..2864e24 Binary files /dev/null and b/graphics/character/jump/2.png differ diff --git a/graphics/font/1.psd b/graphics/font/1.psd new file mode 100644 index 0000000..ec7bd34 Binary files /dev/null and b/graphics/font/1.psd differ diff --git "a/graphics/font/1\320\277\321\200\320\276\320\265\320\272\321\202.psd" "b/graphics/font/1\320\277\321\200\320\276\320\265\320\272\321\202.psd" new file mode 100644 index 0000000..5f1abef Binary files /dev/null and "b/graphics/font/1\320\277\321\200\320\276\320\265\320\272\321\202.psd" differ diff --git a/graphics/font/font.ttf b/graphics/font/font.ttf new file mode 100644 index 0000000..98044e9 Binary files /dev/null and b/graphics/font/font.ttf differ diff --git "a/graphics/font/\320\262\321\213\321\204\320\262\321\204\320\262\321\204\321\213\320\262.psd" "b/graphics/font/\320\262\321\213\321\204\320\262\321\204\320\262\321\204\321\213\320\262.psd" new file mode 100644 index 0000000..a8a0d36 Binary files /dev/null and "b/graphics/font/\320\262\321\213\321\204\320\262\321\204\320\262\321\204\321\213\320\262.psd" differ diff --git a/graphics/game_over_menu/buttons/active/main_menu.png b/graphics/game_over_menu/buttons/active/main_menu.png new file mode 100644 index 0000000..8d7ab7d Binary files /dev/null and b/graphics/game_over_menu/buttons/active/main_menu.png differ diff --git a/graphics/game_over_menu/buttons/active/restart.png b/graphics/game_over_menu/buttons/active/restart.png new file mode 100644 index 0000000..c8f4ddf Binary files /dev/null and b/graphics/game_over_menu/buttons/active/restart.png differ diff --git a/graphics/game_over_menu/buttons/non_active/main_menu.png b/graphics/game_over_menu/buttons/non_active/main_menu.png new file mode 100644 index 0000000..a00cafd Binary files /dev/null and b/graphics/game_over_menu/buttons/non_active/main_menu.png differ diff --git a/graphics/game_over_menu/buttons/non_active/restart.png b/graphics/game_over_menu/buttons/non_active/restart.png new file mode 100644 index 0000000..b41213b Binary files /dev/null and b/graphics/game_over_menu/buttons/non_active/restart.png differ diff --git a/graphics/game_over_menu/game_over.png b/graphics/game_over_menu/game_over.png new file mode 100644 index 0000000..078594d Binary files /dev/null and b/graphics/game_over_menu/game_over.png differ diff --git a/graphics/main_menu/background/background.png b/graphics/main_menu/background/background.png new file mode 100644 index 0000000..ad84ae1 Binary files /dev/null and b/graphics/main_menu/background/background.png differ diff --git a/graphics/main_menu/button/active/exit_btn.png b/graphics/main_menu/button/active/exit_btn.png new file mode 100644 index 0000000..fe29548 Binary files /dev/null and b/graphics/main_menu/button/active/exit_btn.png differ diff --git a/graphics/main_menu/button/active/start_btn.png b/graphics/main_menu/button/active/start_btn.png new file mode 100644 index 0000000..d77d6b5 Binary files /dev/null and b/graphics/main_menu/button/active/start_btn.png differ diff --git a/graphics/main_menu/button/non_active/exit_btn.png b/graphics/main_menu/button/non_active/exit_btn.png new file mode 100644 index 0000000..d6eb676 Binary files /dev/null and b/graphics/main_menu/button/non_active/exit_btn.png differ diff --git a/graphics/main_menu/button/non_active/start_btn.png b/graphics/main_menu/button/non_active/start_btn.png new file mode 100644 index 0000000..4677714 Binary files /dev/null and b/graphics/main_menu/button/non_active/start_btn.png differ diff --git a/graphics/main_menu/marwin.png b/graphics/main_menu/marwin.png new file mode 100644 index 0000000..7f5f48f Binary files /dev/null and b/graphics/main_menu/marwin.png differ diff --git a/graphics/pause/buttons/active/resume.png b/graphics/pause/buttons/active/resume.png new file mode 100644 index 0000000..14e43cd Binary files /dev/null and b/graphics/pause/buttons/active/resume.png differ diff --git a/graphics/pause/buttons/non_active/resume.png b/graphics/pause/buttons/non_active/resume.png new file mode 100644 index 0000000..e2b28c7 Binary files /dev/null and b/graphics/pause/buttons/non_active/resume.png differ diff --git a/graphics/pause/frame.png b/graphics/pause/frame.png new file mode 100644 index 0000000..af4bab6 Binary files /dev/null and b/graphics/pause/frame.png differ diff --git a/graphics/platform/broken/broken-platform.png b/graphics/platform/broken/broken-platform.png new file mode 100644 index 0000000..c614f74 Binary files /dev/null and b/graphics/platform/broken/broken-platform.png differ diff --git a/graphics/platform/normal/platform.png b/graphics/platform/normal/platform.png new file mode 100644 index 0000000..de78b36 Binary files /dev/null and b/graphics/platform/normal/platform.png differ diff --git a/level.py b/level.py index 5cb6954..d1c542b 100644 --- a/level.py +++ b/level.py @@ -1,211 +1,143 @@ -# -*- coding: utf-8 -*- -""" - CopyLeft 2021 Michael Rouves - - This file is part of Pygame-DoodleJump. - Pygame-DoodleJump is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Pygame-DoodleJump is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with Pygame-DoodleJump. If not, see . -""" - - -from random import randint -from pygame import Surface -import asyncio - -from singleton import Singleton -from sprite import Sprite -import settings as config - - - -#return True with a chance of: P(X=True)=1/x -chance = lambda x: not randint(0,x) - - -class Bonus(Sprite): - """ - A class to represent a bonus - Inherits the Sprite class. - """ - - WIDTH = 15 - HEIGHT = 15 - - def __init__(self, parent:Sprite,color=config.GRAY, - force=config.PLAYER_BONUS_JUMPFORCE): - - self.parent = parent - super().__init__(*self._get_inital_pos(), - Bonus.WIDTH, Bonus.HEIGHT, color) - self.force = force - - def _get_inital_pos(self): - x = self.parent.rect.centerx - Bonus.WIDTH//2 - y = self.parent.rect.y - Bonus.HEIGHT - return x,y - - - - - -class Platform(Sprite): - """ - A class to represent a platform. - - Should only be instantiated by a Level instance. - Can have a bonus spring or broke on player jump. - Inherits the Sprite class. - """ - # (Overriding inherited constructor: Sprite.__init__) - def __init__(self, x:int, y:int, width:int, height:int, - initial_bonus=False,breakable=False): - - color = config.PLATFORM_COLOR - if breakable:color = config.PLATFORM_COLOR_LIGHT - super().__init__(x,y,width,height,color) - - self.breakable = breakable - self.__level = Level.instance - self.__bonus = None - if initial_bonus: - self.add_bonus(Bonus) - - # Public getter for __bonus so it remains private - @property - def bonus(self):return self.__bonus - - def add_bonus(self,bonus_type:type) -> None: - """ Safely adds a bonus to the platform. - :param bonus_type type: the type of bonus to add. - """ - assert issubclass(bonus_type,Bonus), "Not a valid bonus type !" - if not self.__bonus and not self.breakable: - self.__bonus = bonus_type(self) - - def remove_bonus(self) -> None: - " Safely removes platform's bonus." - self.__bonus = None - - def onCollide(self) -> None: - " Called in update if collision with player (safe to overrided)." - if self.breakable: - self.__level.remove_platform(self) - - # ( Overriding inheritance: Sprite.draw() ) - def draw(self, surface:Surface) -> None: - """ Like Sprite.draw(). - Also draws the platform's bonus if it has one. - :param surface pygame.Surface: the surface to draw on. - """ - # check if out of screen: should be deleted - super().draw(surface) - if self.__bonus: - self.__bonus.draw(surface) - if self.camera_rect.y+self.rect.height>config.YWIN: - self.__level.remove_platform(self) - - - - - -class Level(Singleton): - """ - A class to represent the level. - - used to manage updates/generation of platforms. - Can be access via Singleton: Level.instance. - (Check Singleton design pattern for more info) - """ - - # constructor called on new instance: Level() - def __init__(self): - self.platform_size = config.PLATFORM_SIZE - self.max_platforms = config.MAX_PLATFORM_NUMBER - self.distance_min = min(config.PLATFORM_DISTANCE_GAP) - self.distance_max = max(config.PLATFORM_DISTANCE_GAP) - - self.bonus_platform_chance = config.BONUS_SPAWN_CHANCE - self.breakable_platform_chance = config.BREAKABLE_PLATFORM_CHANCE - - self.__platforms = [] - self.__to_remove = [] - - self.__base_platform = Platform( - config.HALF_XWIN - self.platform_size[0]//2,# X POS - config.HALF_YWIN + config.YWIN/3, # Y POS - *self.platform_size)# SIZE - - - # Public getter for __platforms so it remains private - @property - def platforms(self) -> list: - return self.__platforms - - - async def _generation(self) -> None: - " Asynchronous management of platforms generation." - # Check how many platform we need to generate - nb_to_generate = self.max_platforms - len(self.__platforms) - for _ in range(nb_to_generate): - self.create_platform() - - - def create_platform(self) -> None: - " Create the first platform or a new one." - if self.__platforms: - # Generate a new random platform : - # x position along screen width - # y position starting from last platform y pos +random offset - offset = randint(self.distance_min,self.distance_max) - self.__platforms.append(Platform( - randint(0,config.XWIN-self.platform_size[0]),# X POS - self.__platforms[-1].rect.y-offset,# Y POS - *self.platform_size, # SIZE - initial_bonus=chance(self.bonus_platform_chance),# HAS A Bonus - breakable=chance(self.breakable_platform_chance)))# IS BREAKABLE - else: - # (just in case) no platform: add the base one - self.__platforms.append(self.__base_platform) - - - def remove_platform(self,plt:Platform) -> bool: - """ Removes a platform safely. - :param plt Platform: the platform to remove - :return bool: returns true if platoform successfully removed - """ - if plt in self.__platforms: - self.__to_remove.append(plt) - return True - return False - - - def reset(self) -> None: - " Called only when game restarts (after player death)." - self.__platforms = [self.__base_platform] - - - def update(self) -> None: - " Should be called each frame in main game loop for generation." - for platform in self.__to_remove: - if platform in self.__platforms: - self.__platforms.remove(platform) - self.__to_remove = [] - asyncio.run(self._generation()) - - - def draw(self,surface:Surface) -> None: - """ Called each frame in main loop, draws each platform - :param surface pygame.Surface: the surface to draw on. - """ - for platform in self.__platforms: - platform.draw(surface) \ No newline at end of file +from random import randint +from pygame import Surface, image, transform, mixer +from pygame.time import Clock, get_ticks +import asyncio +import settings as config +from singleton import Singleton +from sprite import Sprite +mixer.init() + +chance = lambda x: not randint(0,x) # Creating a variable dependent on x, the more x, the less likely it is to get True +class Bonus(Sprite): + def __init__(self, parent:Sprite, force=config.PLAYER_BONUS_JUMPFORCE): # Accepting the parameters of the parent object + self.parent = parent # Binding + super().__init__(*self._get_inital_pos(), + config.BONUS_WIDTH, config.BONUS_HEIGHT) # Position, width, height, color are passed + self.force = force # Jump power + self.model = image.load(config.bonus_default_image).convert_alpha() # Image model + self.effect = False # If player speed increase effect occurs (used to start animation) + self.tag = 'bonus' # tag + self.bonus_effect_duration = 100 # Bonus animation effect duration + self.bonus_effect_start_time = None # Blank for counting the start of the animation + self.current_frame = 0 # Current animation frame + self.animation_finished = False # Animation ended + + def _get_inital_pos(self): + # Getting the current position + x = self.parent.rect.centerx - config.BONUS_WIDTH//2 + y = self.parent.rect.y - config.BONUS_HEIGHT + return x,y + +class Platform(Sprite): + def __init__(self, x:int, y:int, width:int, height:int, + initial_bonus=False,breakable=False): # designation of data types and initial parameters in the class constructor + self.model = image.load(config.platform_image).convert_alpha() # model (default) + self.tag = 'platform' # tag + + if breakable: # Model change if the platform is disposable + self.model = image.load(config.broken_platform_image).convert_alpha() # model (broken) + + super().__init__(x,y,width,height) + self.breakable = breakable + self.__level = Level.instance # Setting value equal to class instance Level + self.__bonus = None # Blank for creating a personal instance of the Bonus class + if initial_bonus: # If the platform have a bonus + self.add_bonus(Bonus) # Adding in a sprite group + + @property # The ability to open access the class + def bonus(self): + return self.__bonus + + def add_bonus(self,bonus_type:type): + # Adds a bonus to the platform + assert issubclass(bonus_type,Bonus) # Checking if bonus_type is a subclass of Bonus + if not self.__bonus and not self.breakable: # Checking whether there is already a bonus on the platform and whether it is a solid platform + self.__bonus = bonus_type(self) # If the conditions match, a bonus instance of type bonus_type is created + + def remove_bonus(self): + self.__bonus = None + + def onCollide(self): + # Removal of the platform if it is a one-time use + if self.breakable: + config.stomp.play() # Breaking sound production + self.__level.remove_platform(self) + + def draw(self, surface:Surface): + # Rendering the platform on the passed Surface + super().draw(surface) # Draw method call + distance = 0 + if self.__bonus: # Checking for the existence of an object on the platform + self.__bonus.draw(surface) + distance = config.BONUS_HEIGHT # Bonus amount + if self.camera_rect.y + self.rect.height > config.YWIN + distance + 5: # Checking if the platform boundary goes beyond the screen + self.__level.remove_platform(self) # Removing a platform + +class Level(Singleton): + def __init__(self): + self.platform_size = config.PLATFORM_SIZE # Platform size + self.max_platforms = config.MAX_PLATFORM_NUMBER # Max platform count + self.distance_min = min(config.PLATFORM_DISTANCE_GAP) # Minimum distance between platforms + self.distance_max = max(config.PLATFORM_DISTANCE_GAP) # Maximum distance between platforms + + + self.bonus_platform_chance = config.BONUS_SPAWN_CHANCE # Bonus spawn chance + self.breakable_platform_chance = config.BREAKABLE_PLATFORM_CHANCE # Disposable platform chance + + self.__platforms = [] + self.__to_remove = [] # Whatever needs to be removed + + # Creating a platform instance + self.__base_platform = Platform( + config.HALF_XWIN - self.platform_size[0]//2, # X pos + config.HALF_YWIN + config.YWIN/3, # Y pos + *self.platform_size) # Size + + # Definition as a class property + @property + def platforms(self) -> list: # Expected data type: list + return self.__platforms # Returns all platforms as list + + # Asynchronous function (asynchrony is used to optimize and process the function by several processor cores) + async def _generation(self): + # Checking the required number of platforms for generation + nb_to_generate = self.max_platforms - len(self.__platforms) # Calculation of the number of platforms for generation + for _ in range(nb_to_generate): # Creating a platforms + self.create_platform() + + def create_platform(self): + # Creating a new platforms + if self.__platforms: # If platform exists + # Generation a random platform + offset = randint(self.distance_min,self.distance_max) # Mixing relative to neighbors + self.__platforms.append(Platform( # Creating a new instance + randint(0,config.XWIN-self.platform_size[0]), # X pos + self.__platforms[-1].rect.y-offset, # Y pos + *self.platform_size, # Size + initial_bonus=chance(self.bonus_platform_chance), # Bonus available + breakable=chance(self.breakable_platform_chance))) # Disposable or not + else: + # If the platform does not exist, adds the base platform to the list + self.__platforms.append(self.__base_platform) + + def remove_platform(self,plt:Platform) -> bool: # Return value bool + if plt in self.__platforms: + self.__to_remove.append(plt) + return True + return False # Platform has not found + + def reset(self): + # Platform reloading, by assigning platforms to one base platform + self.__platforms = [self.__base_platform] + + def update(self): + # Called every frame to generate + for platform in self.__to_remove: + if platform in self.__platforms: + self.__platforms.remove(platform) + self.__to_remove = [] + asyncio.run(self._generation()) # Running a function using async + + def draw(self,surface:Surface): + for platform in self.__platforms: # Rendering of each platform + platform.draw(surface) diff --git a/main.py b/main.py index 727970f..553e22e 100644 --- a/main.py +++ b/main.py @@ -1,135 +1,330 @@ -# -*- coding: utf-8 -*- -""" - CopyLeft 2021 Michael Rouves - - This file is part of Pygame-DoodleJump. - Pygame-DoodleJump is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Pygame-DoodleJump is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with Pygame-DoodleJump. If not, see . -""" - - +from MARWIN_ADVENTURE import * +from button import Button import pygame, sys - from singleton import Singleton from camera import Camera from player import Player from level import Level import settings as config +pygame.init() +# Window +window = pygame.display.set_mode(config.DISPLAY,config.FLAGS) +clock = pygame.time.Clock() +# Function for displaying text +def display_text(text, font_size, color, font_path, pos): + font = pygame.font.Font(font_path, font_size) + text_render = font.render(text, True, color) + text_rect = text_render.get_rect(center=pos) + window.blit(text_render, text_rect) class Game(Singleton): - """ - A class to represent the game. + # Class for managing the game + def __init__(self): + # Initialization + self.score = 0 # Record + self.paused = False # Pause + self.max_darkness_reached = False # Has maximum darkness been reached + self.frame_reverse_animation_end = True # End of reverse frame animation + self.frame_animation_end = False # End of frame animation + self.game_over_animation_end = False # End of game over image animation + # End of button animation + self.mainmenu_button_animation_end = False + self.restart_button_animation_end = False - used to manage game updates, draw calls and user input events. - Can be access via Singleton: Game.instance . - (Check Singleton design pattern for more info) - """ + # Game over + self.game_over_image = pygame.image.load(config.game_over_image).convert() + self.gameover_rect_initial_y = -config.HALF_YWIN # Initial position of game over vertically + self.gameover_rect_y = self.gameover_rect_initial_y # Current offset of game over vertically + self.gameover_rect = self.game_over_image.get_rect(center=(config.HALF_XWIN,config.HALF_YWIN//3)) # Get image size + self.game_over_sound_played = False - # constructor called on new instance: Game() - def __init__(self) -> None: + # Frame images for pause, score + self.frame_image_pause = pygame.image.load(config.frame_image_pause).convert() + self.frame_image_score = pygame.image.load(config.frame_image_score).convert() + self.frame_rect_initial_y = -20 # Initial position of frame vertically + self.frame_rect_y = self.frame_rect_initial_y # Current offset of frame vertically - # ============= Initialisation ============= - self.__alive = True - # Window / Render - self.window = pygame.display.set_mode(config.DISPLAY,config.FLAGS) - self.clock = pygame.time.Clock() - - # Instances - self.camera = Camera() - self.lvl = Level() - self.player = Player( - config.HALF_XWIN - config.PLAYER_SIZE[0]/2,# X POS - config.HALF_YWIN + config.HALF_YWIN/2,# Y POS - *config.PLAYER_SIZE,# SIZE - config.PLAYER_COLOR# COLOR - ) - - # User Interface - self.score = 0 - self.score_txt = config.SMALL_FONT.render("0 m",1,config.GRAY) - self.score_pos = pygame.math.Vector2(10,10) - - self.gameover_txt = config.LARGE_FONT.render("Game Over",1,config.GRAY) - self.gameover_rect = self.gameover_txt.get_rect( - center=(config.HALF_XWIN,config.HALF_YWIN)) - - - def close(self): - self.__alive = False + # Buttons: mainmenu, restart, resume + self.mainmenu_button_rect_initial_x = 0 # Initial position of mainmenu horizontally + self.restart_button_rect_initial_x = config.XWIN # Initial position of restart horizontally + self.mainmenu_button_image = pygame.image.load(config.mainmenu_button_non_active) + self.restart_button_image = pygame.image.load(config.restart_button_non_active) + self.resume_button_image = pygame.image.load(config.resume_button_non_active) + # Button sounds + self.mainmenu_button_sound = False + self.restart_button_sound = False + self.resume_button_sound = False - def reset(self): - self.camera.reset() - self.lvl.reset() - self.player.reset() + # Background + self.background_image = pygame.image.load(config.background_image).convert() + self.background_image = pygame.transform.scale(self.background_image, (config.XWIN, config.YWIN)) + self.overlay_color = config.BLACK # Dimming color + self.overlay_alpha = 200 # Dimming alpha channel + ######################Class instances###################### + # Buttons + #For game over + self.mainmenu_button = Button(self.mainmenu_button_image, (self.mainmenu_button_rect_initial_x, config.YWIN // 1.2), action='') + self.restart_button = Button(self.restart_button_image, (self.restart_button_rect_initial_x, config.YWIN // 1.2), action='') + # For pause + self.mainmenu_button_pause = Button(self.mainmenu_button_image, (config.XWIN // 4, config.YWIN // 2), action='') + self.restart_button_pause = Button(self.restart_button_image, (config.XWIN // 2, config.YWIN // 2), action='') + self.resume_button = Button(self.resume_button_image, (config.XWIN // 1.34, config.YWIN // 2), action='') + # Objects + self.camera = Camera() # Camera + self.lvl = Level() # Level + self.player = Player( # Player + config.HALF_XWIN - config.PLAYER_SIZE[0]/2, # X position + config.HALF_YWIN + config.HALF_YWIN/2, # Y position + *config.PLAYER_SIZE,) # Size + + def resume(self): # Resume pause + self.paused = False + + def reset(self): # Reset the game + if self.game_over_animation_end or self.paused: + pygame.mixer.music.play(-1) + self.paused = False + self.camera.reset() + self.lvl.reset() + self.player.reset() + + def launch_main_menu(self): # Launch main menu and exit current loop + import MARWIN_ADVENTURE + MARWIN_ADVENTURE.Run() + pygame.quit() + sys.exit() - def _event_loop(self): - # ---------- User Events ---------- + def _event_loop(self): # Event handling + # User events + mouse_pos = pygame.mouse.get_pos() for event in pygame.event.get(): if event.type == pygame.QUIT: - self.close() - elif event.type == pygame.KEYDOWN: - if event.key == pygame.K_ESCAPE: - self.close() - if event.key == pygame.K_RETURN and self.player.dead: - self.reset() - self.player.handle_event(event) + #self.close() + pygame.quit() + sys.exit() + elif event.type == pygame.KEYDOWN: + if event.key == pygame.K_ESCAPE: # Pause + self.paused = not self.paused # Toggle value + elif event.type == pygame.MOUSEBUTTONDOWN: + if self.player.dead: # If game over, process menu buttons to prevent false clicks + self.mainmenu_button.check_click(mouse_pos) + self.restart_button.check_click(mouse_pos) + if self.paused: # If paused, process pause menu buttons + self.resume_button.check_click(mouse_pos) + self.mainmenu_button_pause.check_click(mouse_pos) + self.restart_button_pause.check_click(mouse_pos) + if not self.paused: # If not paused, process user events + self.player.handle_event(event) def _update_loop(self): - # ----------- Update ----------- + # Update surfaces self.player.update() self.lvl.update() if not self.player.dead: - self.camera.update(self.player.rect) - #calculate score and update UI txt - self.score=-self.camera.state.y//50 - self.score_txt = config.SMALL_FONT.render( - str(self.score)+" m", 1, config.GRAY) - + self.camera.update(self.player.rect) # Update camera movement + # Calculate score and display it + self.score=-self.camera.state.y//50 # Update score def _render_loop(self): - # ----------- Display ----------- - self.window.fill(config.WHITE) - self.lvl.draw(self.window) - self.player.draw(self.window) + global window, clock + # Display objects - # User Interface - if self.player.dead: - self.window.blit(self.gameover_txt,self.gameover_rect)# gameover txt - self.window.blit(self.score_txt, self.score_pos)# score txt + # Calculate background offset based on camera position + offset_y = -self.camera.state.y % config.YWIN + # Display background + window.blit(self.background_image, (0, offset_y)) + window.blit(self.background_image, (0, offset_y - config.YWIN)) - pygame.display.update()# window update - self.clock.tick(config.FPS)# max loop/s + # Draw objects using Sprite.py + self.lvl.draw(window) + self.player.draw(window) + # Assign functions to buttons + # Buttons for game over + self.mainmenu_button.action = self.launch_main_menu + self.restart_button.action = self.reset + # Buttons for pause + self.resume_button.action = self.resume + self.mainmenu_button_pause.action = self.launch_main_menu + self.restart_button_pause.action = self.reset + + ################## DRAWING OBJECTS BASED ON EVENTS ################## + if not self.player.dead and self.paused: # Pause + mouse_pos = pygame.mouse.get_pos() # Get current mouse position + window.blit(self.frame_image_pause, (pygame.math.Vector2(config.HALF_XWIN - 185, config.YWIN // 4 - 50))) # Draw frame for text + display_text('PAUSE', 65, config.WHITE, config.pixel_font, (pygame.math.Vector2(config.HALF_XWIN, config.YWIN // 4))) # Draw "PAUSE" text + + # Handle button hover reactions + # Resume button + if self.resume_button.rect.collidepoint(mouse_pos): # If collision, switch to active image + if self.resume_button_sound: # Play button sound only once + config.check_button.play() + self.resume_button_sound = False + window.blit(pygame.image.load(config.resume_button_active).convert(), self.resume_button.rect) + else: # Change to default image + self.resume_button_sound = True + window.blit(self.resume_button.image.convert(), self.resume_button.rect) - def run(self): - # ============= MAIN GAME LOOP ============= - while self.__alive: - self._event_loop() - self._update_loop() - self._render_loop() - pygame.quit() + # Main menu + if self.mainmenu_button_pause.rect.collidepoint(mouse_pos): # If there is collision, change to active image + if self.mainmenu_button_sound: # Play button sound only once + config.check_button.play() + self.mainmenu_button_sound = False + window.blit(pygame.image.load(config.mainmenu_button_active).convert(), self.mainmenu_button_pause.rect) + else: # Change to default image + self.mainmenu_button_sound = True + window.blit(self.mainmenu_button_pause.image.convert(), self.mainmenu_button_pause.rect) + # Restart + if self.restart_button_pause.rect.collidepoint(mouse_pos): # If there is collision, change to active image + if self.restart_button_sound: # Play button sound only once + config.check_button.play() + self.restart_button_sound = False + window.blit(pygame.image.load(config.restart_button_active).convert(), self.restart_button_pause.rect) + else: # Change to default image + self.restart_button_sound = True + window.blit(self.restart_button_pause.image.convert(), self.restart_button_pause.rect) + if self.player.dead: # Game over + # Zeroing the results of the frame if the animation is over + if self.frame_animation_end: + self.frame_rect_y = self.frame_rect_initial_y + self.frame_animation_end = False + self.frame_reverse_animation_end = False + if config.music_volume >= 0.0: + config.music_volume -= 0.001 + pygame.mixer.music.set_volume(config.music_volume) -if __name__ == "__main__": - # ============= PROGRAM STARTS HERE ============= + # Screen dimming + self.overlay_alpha = min(self.overlay_alpha + 5, 200) + if self.overlay_alpha >= 200: + self.max_darkness_reached = True + + # Animation of moving buttons and images + # Game over + if self.gameover_rect_y < 15: + self.gameover_rect_y += 10 + else: + self.game_over_animation_end = True + + # Main menu button + if self.mainmenu_button.rect.x < config.XWIN // 3 - self.mainmenu_button.rect[2] // 2: + self.mainmenu_button.rect.x += 10 + else: + self.mainmenu_button_animation_end = True + + # Restart button + if self.restart_button.rect.x > config.XWIN // 1.5 - self.restart_button.rect[2] // 2: + self.restart_button.rect.x -= 13 + else: + self.restart_button_animation_end = True + + else: + # Zeroing values + # Decrease the alpha channel of the fade over time (this is where the fade rate is set) + if config.music_volume < 0.1: + config.music_volume += 0.01 + pygame.mixer.music.set_volume(config.music_volume) + self.game_over_sound_played = False + self.overlay_alpha = max(self.overlay_alpha - 5, 0) + self.max_darkness_reached = False + self.game_over_animation_end = False + self.mainmenu_button_animation_end = False + self.restart_button_animation_end = False + self.score_txt_visible = True + self.gameover_rect_y = self.gameover_rect_initial_y + self.mainmenu_button.rect.x = self.mainmenu_button_rect_initial_x + self.restart_button.rect.x = self.restart_button_rect_initial_x + + # Creating a surface and applying shading to it + overlay_surface = pygame.Surface((config.XWIN, config.YWIN)) + overlay_surface.fill(self.overlay_color) + overlay_surface.set_alpha(self.overlay_alpha) + if self.overlay_alpha != 0: + self.score_txt_visible = False + window.blit(overlay_surface, (0, 0)) + + # Game process + if self.score_txt_visible or not self.frame_reverse_animation_end: + # High score counter animation + if not self.frame_animation_end and self.score_txt_visible: + if self.frame_rect_y < 10: + self.frame_rect_y += 2 + else: + self.frame_animation_end = True + + # High score counter animation + elif not self.frame_reverse_animation_end: + if self.frame_rect_y >= -50: + self.frame_rect_y -= 10 + else: + self.frame_reverse_animation_end = True + + # Displaying + window.blit(self.frame_image_score, (config.XWIN // 6, self.frame_rect_y)) # Frame + display_text(str(self.score) + "m", 30, config.WHITE, config.pixel_font, (pygame.math.Vector2(config.XWIN // 3.1, self.frame_rect_y + 25))) # Score text + + # Displaying game over objects + if self.max_darkness_reached: # if darkening ended + if not self.game_over_sound_played: + config.game_over.play() + self.game_over_sound_played = True + + window.blit(self.game_over_image, (config.HALF_XWIN - self.gameover_rect.width // 2, self.gameover_rect_y)) # Displaying game over images + display_text(" SCORE:" + str(self.score) + "m", 40, config.WHITE, config.pixel_font, (config.HALF_XWIN, self.gameover_rect_y + config.YWIN // 1.5)) # Displaying score + mouse_pos = pygame.mouse.get_pos() # Getting mouse position + + # Processing the reaction of buttons on hovering the cursor + # Main menu + if self.mainmenu_button.rect.collidepoint(mouse_pos) and self.game_over_animation_end: # If there is collision, change to active image + if self.mainmenu_button_sound: # Play button sound only once + config.check_button.play() + self.mainmenu_button_sound = False + window.blit(pygame.image.load(config.mainmenu_button_active).convert(), self.mainmenu_button.rect) + else: # Change to default image + self.mainmenu_button_sound = True + window.blit(self.mainmenu_button.image.convert(), self.mainmenu_button.rect) + + # Restart + if self.restart_button.rect.collidepoint(mouse_pos) and self.game_over_animation_end: # If there is collision, change to active image + if self.restart_button_sound: # Play button sound only once + config.check_button.play() + self.restart_button_sound = False + window.blit(pygame.image.load(config.restart_button_active).convert(), self.restart_button.rect) + else: # Change to default image + self.restart_button_sound = True + window.blit(self.restart_button.image.convert(), self.restart_button.rect) + + # Updating scene + pygame.display.update() + clock.tick(config.FPS) + + def run(self): + # Main game loop + pygame.mixer.music.load(config.level_music) + pygame.mixer.music.set_volume(config.music_volume) + pygame.mixer.music.play(-1) + while True: + self._event_loop() # Event handling + self._render_loop() # Event render + if not self.paused: + self._update_loop() # Update surface + +def run_game(): # Game launch function game = Game() game.run() +if __name__ == "__main__": + # Program start + run_game() + + + + diff --git a/player.py b/player.py index 4b80fa8..e16fe67 100644 --- a/player.py +++ b/player.py @@ -1,153 +1,128 @@ -# -*- coding: utf-8 -*- -""" - CopyLeft 2021 Michael Rouves - - This file is part of Pygame-DoodleJump. - Pygame-DoodleJump is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Pygame-DoodleJump is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with Pygame-DoodleJump. If not, see . -""" - - from math import copysign -from pygame.math import Vector2 -from pygame.locals import KEYDOWN,KEYUP,K_LEFT,K_RIGHT -from pygame.sprite import collide_rect -from pygame.event import Event - +import pygame from singleton import Singleton from sprite import Sprite from level import Level import settings as config +pygame.mixer.init() - -#Return the sign of a number: getsign(-5)-> -1 -getsign = lambda x : copysign(1, x) +# Returning values: (x < 0 => -1) & (x > 0 => 1) & (x == 0 => 0) +getsign = lambda x: copysign(1, x) class Player(Sprite, Singleton): - """ - A class to represent the player. - - Manages player's input,physics (movement...). - Can be access via Singleton: Player.instance. - (Check Singleton design pattern for more info). - """ - # (Overriding Sprite.__init__ constructor) - def __init__(self,*args): - #calling default Sprite constructor - Sprite.__init__(self,*args) - self.__startrect = self.rect.copy() - self.__maxvelocity = Vector2(config.PLAYER_MAX_SPEED,100) - self.__startspeed = 1.5 - - self._velocity = Vector2() - self._input = 0 - self._jumpforce = config.PLAYER_JUMPFORCE - self._bonus_jumpforce = config.PLAYER_BONUS_JUMPFORCE - - self.gravity = config.GRAVITY - self.accel = .5 - self.deccel = .6 - self.dead = False - - - def _fix_velocity(self) -> None: - """ Set player's velocity between max/min. - Should be called in Player.update(). - """ - self._velocity.y = min(self._velocity.y,self.__maxvelocity.y) - self._velocity.y = round(max(self._velocity.y,-self.__maxvelocity.y),2) - self._velocity.x = min(self._velocity.x,self.__maxvelocity.x) - self._velocity.x = round(max(self._velocity.x,-self.__maxvelocity.x),2) - - - def reset(self) -> None: - " Called only when game restarts (after player death)." - self._velocity = Vector2() - self.rect = self.__startrect.copy() - self.camera_rect = self.__startrect.copy() - self.dead = False - - - def handle_event(self,event:Event) -> None: - """ Called in main loop foreach user input event. - :param event pygame.Event: user input event - """ - # Check if start moving - if event.type == KEYDOWN: - # Moves player only on x-axis (left/right) - if event.key == K_LEFT: - self._velocity.x=-self.__startspeed - self._input = -1 - elif event.key == K_RIGHT: - self._velocity.x=self.__startspeed - self._input = 1 - #Check if stop moving - elif event.type == KEYUP: - if (event.key== K_LEFT and self._input==-1) or ( - event.key==K_RIGHT and self._input==1): - self._input = 0 - - - def jump(self,force:float=None) -> None: - if not force:force = self._jumpforce - self._velocity.y = -force - - - def onCollide(self, obj:Sprite) -> None: - self.rect.bottom = obj.rect.top - self.jump() - - - def collisions(self) -> None: - """ Checks for collisions with level. - Should be called in Player.update(). - """ - lvl = Level.instance - if not lvl: return - for platform in lvl.platforms: - # check falling and colliding <=> isGrounded ? - if self._velocity.y > .5: - # check collisions with platform's spring bonus - if platform.bonus and collide_rect(self,platform.bonus): - self.onCollide(platform.bonus) - self.jump(platform.bonus.force) - - # check collisions with platform - if collide_rect(self,platform): - self.onCollide(platform) - platform.onCollide() - - - def update(self) -> None: - """ For position and velocity updates. - Should be called each frame. - """ - #Check if player out of screen: should be dead - if self.camera_rect.y>config.YWIN*2: - self.dead = True - return - #Velocity update (apply gravity, input acceleration) - self._velocity.y += self.gravity - if self._input: # accelerate - self._velocity.x += self._input*self.accel - elif self._velocity.x: # deccelerate - self._velocity.x -= getsign(self._velocity.x)*self.deccel - self._velocity.x = round(self._velocity.x) - self._fix_velocity() - - #Position Update (prevent x-axis to be out of screen) - self.rect.x = (self.rect.x+self._velocity.x)%(config.XWIN-self.rect.width) - self.rect.y += self._velocity.y - - self.collisions() \ No newline at end of file + # Initialization using a shared dictionary + def __init__(self, *args): + Sprite.__init__(self, *args) + self.__startrect = self.rect.copy() # Create the main rect + self.__maxvelocity = pygame.Vector2(config.PLAYER_MAX_SPEED, 100) # Create a vector with x, y coordinates + self.__startspeed = 1.5 # Initial speed, increases over time + self._velocity = pygame.Vector2() # Create a vector + self._input = 0 # Input check + self._jumpforce = config.PLAYER_JUMPFORCE # Jump force + self._bonus_jumpforce = config.PLAYER_BONUS_JUMPFORCE # Bonus jump force + self.gravity = config.GRAVITY # Gravity + self.accel = 0.5 # Acceleration + self.deccel = 0.6 # Deceleration + self.dead = False # Whether dead or not + self.model = pygame.image.load(config.jumping[0]).convert_alpha() # Model + self.tag = 'player' # Tag + self.direction = '' # Direction + self.condition = '' # Current condition + + def _fix_velocity(self): + # Set velocity between minimum and maximum values + self._velocity.y = min(self._velocity.y, self.__maxvelocity.y) + self._velocity.y = round(max(self._velocity.y, -self.__maxvelocity.y), 2) + self._velocity.x = min(self._velocity.x, self.__maxvelocity.x) + self._velocity.x = round(max(self._velocity.x, -self.__maxvelocity.x), 2) + + def reset(self): + # Reset the game + self._velocity = pygame.Vector2() + self.rect = self.__startrect.copy() + self.camera_rect = self.__startrect.copy() + self.dead = False + + def handle_event(self, event: pygame.event.Event): + # Check for movement initiation + if event.type == pygame.KEYDOWN: + # Left-right movement + if event.key == pygame.K_a: # Left + self._velocity.x = -self.__startspeed + self._input = -1 + self.left = True + self.right = False + self.direction = 'left' + elif event.key == pygame.K_d: # Right + self._velocity.x = self.__startspeed + self._input = 1 + self.right = True + self.left = False + self.direction = 'right' + + # Check for movement cessation + elif event.type == pygame.KEYUP: + if (event.key == pygame.K_a and self._input == -1) or (event.key == pygame.K_d and self._input == 1): + self._input = 0 + + def jump(self, force: float = None): + # Jump + if not force: # If force is None or False + force = self._jumpforce # Assign jump force + self._velocity.y = -force # Change the y-component of the velocity vector + + def onCollide(self, obj: Sprite): + # Collision with the upper part of a platform + self.rect.bottom = obj.rect.top + self.jump() + + def get_status(self): + # Get movement status + if 0.5 > self._velocity.y >= -21: + self.condition = 'jumping' + elif self._velocity.y < -21: + self.condition = 'bonus_effect' + else: + self.condition = 'falling' + + def collisions(self): + # Collisions + lvl = Level.instance # Instance of the Level class + if not lvl: # If the instance doesn't exist, return None and stop execution + return + for platform in lvl.platforms: # Iterate through platforms + # Check for falling or collision + if self._velocity.y > 0.5: # Vertical speed is greater, indicating the object should fall + # Check for collision with a bonus + if platform.bonus and pygame.sprite.collide_rect(self, platform.bonus): + config.explosion.play() # Play explosion sound + self.onCollide(platform.bonus) + platform.bonus.effect = True + self.jump(platform.bonus.force) # Apply higher jump force + self.bonus_effect_start_time = pygame.time.get_ticks() + # Check for collision with a platform + if pygame.sprite.collide_rect(self, platform): + config.jump.play() # Play jump sound + self.onCollide(platform) + platform.onCollide() + + def update(self): + # Player movement + if self.camera_rect.y > config.YWIN * 2: + self.dead = True + return + # Update velocity (applying gravity, input data) + self._velocity.y += self.gravity # Change velocity based on gravity + if self._input: # Accelerate along the x-axis when a key is pressed + self._velocity.x += self._input * self.accel + elif self._velocity.x: # Decelerate along the x-axis when no key is pressed + self._velocity.x -= getsign(self._velocity.x) * self.deccel + self._velocity.x = round(self._velocity.x) + self._fix_velocity() + + # Update position + self.rect.x = (self.rect.x + self._velocity.x) % (config.XWIN - self.rect.width) # Update horizontal position + self.rect.y += self._velocity.y # Update vertical position + + self.collisions() diff --git a/requirements.txt b/requirements.txt index ac7421d..dc22d56 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1 +1,2 @@ -pygame==2.0.1 \ No newline at end of file +Pygame 2.5.1 +Python 3.7.7 diff --git a/screenshots/preview_1.jpg b/screenshots/preview_1.jpg new file mode 100644 index 0000000..d2737c4 Binary files /dev/null and b/screenshots/preview_1.jpg differ diff --git a/screenshots/preview_2.jpg b/screenshots/preview_2.jpg new file mode 100644 index 0000000..b009ad0 Binary files /dev/null and b/screenshots/preview_2.jpg differ diff --git a/screenshots/preview_3.jpg b/screenshots/preview_3.jpg new file mode 100644 index 0000000..b564f53 Binary files /dev/null and b/screenshots/preview_3.jpg differ diff --git a/screenshots/preview_4.jpg b/screenshots/preview_4.jpg new file mode 100644 index 0000000..5e758ac Binary files /dev/null and b/screenshots/preview_4.jpg differ diff --git a/settings.py b/settings.py index 83a6768..cb57d12 100644 --- a/settings.py +++ b/settings.py @@ -1,41 +1,122 @@ -# -*- coding: utf-8 -*- -from pygame.font import SysFont -from pygame import init -init() -# ================================== - -#Window Settings -XWIN, YWIN = 600,800 # Resolution -HALF_XWIN,HALF_YWIN = XWIN/2,YWIN/2 # Center -DISPLAY = (XWIN,YWIN) -FLAGS = 0 # Fullscreen, resizeable... -FPS = 60 # Render frame rate +import pygame +from button import Button +pygame.init() + +# Video settings +XWIN, YWIN = 800, 800 # Resolution +HALF_XWIN, HALF_YWIN = XWIN / 2, YWIN / 2 # Screen center +DISPLAY = (XWIN, YWIN) # Create screen instance +FLAGS = pygame.DOUBLEBUF # Screen options: 0 - none, pygame.FULLSCREEN - fullscreen, pygame.DOUBLEBUF - double buffering, pygame.SCALED - scaling +FPS = 60 + +# Fonts +pixel_font = 'graphics\\font\\font.ttf' # Colors -BLACK = (0,0,0) -WHITE = (255,255,255) -GRAY = (100,100,100) -LIGHT_GREEN = (131,252,107) -ANDROID_GREEN = (164,198,57) -FOREST_GREEN = (87,189,68) +BLACK = (0, 0, 0) +WHITE = (255, 255, 255) +GRAY = (100, 100, 100) +LIGHT_GREEN = (131, 252, 107) +ANDROID_GREEN = (164, 198, 57) +FOREST_GREEN = (87, 189, 68) +DARK_BLUE = (2, 3, 28) # Player -PLAYER_SIZE = (25,35) -PLAYER_COLOR = ANDROID_GREEN -PLAYER_MAX_SPEED = 20 -PLAYER_JUMPFORCE = 20 -PLAYER_BONUS_JUMPFORCE = 70 -GRAVITY = .98 - -# Platforms -PLATFORM_COLOR = FOREST_GREEN -PLATFORM_COLOR_LIGHT = LIGHT_GREEN -PLATFORM_SIZE = (100,10) -PLATFORM_DISTANCE_GAP = (50,210) -MAX_PLATFORM_NUMBER = 10 -BONUS_SPAWN_CHANCE = 10 -BREAKABLE_PLATFORM_CHANCE = 12 +PLAYER_SIZE = (90, 60) # Size: width, height +PLAYER_MAX_SPEED = 20 # Maximum speed +PLAYER_JUMPFORCE = 20 # Maximum acceleration +PLAYER_BONUS_JUMPFORCE = 70 # Maximum bonus acceleration +GRAVITY = 0.92 # Gravity -# Fonts -LARGE_FONT = SysFont("",128) -SMALL_FONT = SysFont("arial",24) \ No newline at end of file +# Platform +PLATFORM_SIZE = (110, 10) # Platform size +PLATFORM_DISTANCE_GAP = (50, 130) # Distance between platforms +MAX_PLATFORM_NUMBER = 12 # Maximum number of platforms for generation +BONUS_WIDTH = 35 # Bonus width +BONUS_HEIGHT = 35 # Bonus height +BONUS_SPAWN_CHANCE = 6 # Bonus spawn chance +BREAKABLE_PLATFORM_CHANCE = 6 # Breakable platform spawn chance + +# Music +music_volume = 0.2 +level_music = 'audio\\level_music.wav' +overworld_music = 'audio\\overworld_music.wav' + +# Sounds +sound_volume = 1 +activate_button = 'audio\\effects\\activate_button.ogg' +check_button = 'audio\\effects\\check_button.ogg' +jump = 'audio\\effects\\jump.ogg' +stomp = 'audio\\effects\\stomp.ogg' +explosion = 'audio\\effects\\explosion.ogg' +game_over = 'audio\\effects\\game_over.ogg' + +activate_button = pygame.mixer.Sound(activate_button) +activate_button.set_volume(1) + +check_button = pygame.mixer.Sound(check_button) +check_button.set_volume(1) + +jump = pygame.mixer.Sound(jump) +jump.set_volume(0.1) + +stomp = pygame.mixer.Sound(stomp) +stomp.set_volume(1) + +explosion = pygame.mixer.Sound(explosion) +explosion.set_volume(1) + +game_over = pygame.mixer.Sound(game_over) +game_over.set_volume(0.5) + +# Animation +jumping = [ # Jumping animation + 'graphics\\character\\jump\\1.png', +] + +falling = [ # Falling animation + 'graphics\\character\\fall\\2.png', + 'graphics\\character\\fall\\3.png', + 'graphics\\character\\fall\\4.png', + 'graphics\\character\\fall\\5.png', +] + +bonus_explosion = [ # Bonus explosion animation + 'graphics\\bonus\\explosion\\1.png', + 'graphics\\bonus\\explosion\\2.png', + 'graphics\\bonus\\explosion\\3.png', + 'graphics\\bonus\\explosion\\4.png', + 'graphics\\bonus\\explosion\\5.png', + 'graphics\\bonus\\explosion\\6.png', +] + +# Images +# For gameplay +background_image = 'graphics\\background\\background.png' +bonus_default_image = 'graphics\\bonus\\defaultsituation\\power_keg_with_m.png' +platform_image = 'graphics\\platform\\normal\\platform.png' +broken_platform_image = 'graphics\\platform\\broken\\broken-platform.png' + +# For the main menu +main_menu_background = 'graphics\\main_menu\\background\\background.png' +exit_button_non_active = 'graphics\\main_menu\\button\\non_active\\exit_btn.png' +start_button_non_active = 'graphics\\main_menu\\button\\non_active\\start_btn.png' +exit_button_active = 'graphics\\main_menu\\button\\active\\exit_btn.png' +start_button_active = 'graphics\\main_menu\\button\\active\\start_btn.png' +marwin_image = 'graphics\\main_menu\\marwin.png' + +# For the game over and pause menus +game_over_image = 'graphics\\game_over_menu\\game_over.png' +restart_button_non_active = 'graphics\\game_over_menu\\buttons\\non_active\\restart.png' +mainmenu_button_non_active = 'graphics\\game_over_menu\\buttons\\non_active\\main_menu.png' +resume_button_non_active = 'graphics\\pause\\buttons\\non_active\\resume.png' +restart_button_active = 'graphics\\game_over_menu\\buttons\\active\\restart.png' +mainmenu_button_active = 'graphics\\game_over_menu\\buttons\\active\\main_menu.png' +resume_button_active = 'graphics\\pause\\buttons\\active\\resume.png' + +# UI +frame_image_score = 'graphics\\UI\\frame.png' +frame_image_pause = 'graphics\\pause\\frame.png' + +# Icon +icon = 'graphics\\UI\\m.png' diff --git a/singleton.py b/singleton.py index 9f08729..81dcccb 100644 --- a/singleton.py +++ b/singleton.py @@ -1,32 +1,12 @@ -# -*- coding: utf-8 -*- -""" - CopyLeft 2021 Michael Rouves - - This file is part of Pygame-DoodleJump. - Pygame-DoodleJump is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Pygame-DoodleJump is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with Pygame-DoodleJump. If not, see . -""" - - - +# Singleton allows creating only one instance of the class and provides a global access point to this instance, +# to avoid AttributeError class Singleton: - """ - Singleton pattern. - Overload only class that must have one instance. - Stores the instance in a static variable: Class.instance - (Check Singleton design pattern for more info) - """ - def __new__(cls,*args,**kwargs): - if not hasattr(cls, 'instance'): - cls.instance = super(Singleton, cls).__new__(cls) - return cls.instance + def __new__(cls, *args, **kwargs): + # Handles the process of creating an instance of the class + if not hasattr(cls, 'instance'): + # Check if the 'instance' attribute exists in the class 'cls'. + # If it doesn't exist, it means an instance of the class hasn't been created yet. + cls.instance = super(Singleton, cls).__new__(cls) + # Create a new instance of the class + return cls.instance + # Returns the instance of the class diff --git a/sprite.py b/sprite.py index 3ea9f93..7fd7f19 100644 --- a/sprite.py +++ b/sprite.py @@ -1,68 +1,57 @@ -# -*- coding: utf-8 -*- -""" - CopyLeft 2021 Michael Rouves - - This file is part of Pygame-DoodleJump. - Pygame-DoodleJump is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - Pygame-DoodleJump is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with Pygame-DoodleJump. If not, see . -""" - - -from pygame import Surface,Rect +import settings as config from camera import Camera - - +import pygame class Sprite: - """ - A class to represent a sprite. - - Used for pygame displaying. - Image generated with given color and size. - """ - # default constructor (must be called if overrided by inheritance) - def __init__(self,x:int,y:int,w:int,h:int,color:tuple): - self.__color = color - self._image = Surface((w,h)) - self._image.fill(self.color) - self._image = self._image.convert() - self.rect = Rect(x,y,w,h) - self.camera_rect = self.rect.copy() - - # Public getters for _image & __color so they remain private - @property - def image(self) -> Surface: - return self._image - @property - def color(self) -> tuple: - return self.__color - - @color.setter - def color(self, new:tuple) -> None: - " Called when Sprite.__setattr__('color',x)." - assert isinstance(new,tuple) and len(new)==3,"Value is not a color" - self.__color = new - #update image surface - self._image.fill(self.color) - - - def draw(self, surface:Surface) -> None: - """ Render method,Should be called every frame after update. - :param surface pygame.Surface: the surface to draw on. - """ - # If camera instancied: calculate render positon - if Camera.instance: - self.camera_rect = Camera.instance.apply(self) - surface.blit(self._image,self.camera_rect) - else: - surface.blit(self._image,self.rect) \ No newline at end of file + # Constructor for the sprite model + def __init__(self, x: int, y: int, w: int, h: int): + self.width = w + self.height = h + self.rect = pygame.Rect(x, y, w, h) # Create a rectangle to define the sprite's position on the screen + self.camera_rect = self.rect.copy() # Track the sprite's position for the camera + + def draw(self, surface: pygame.Surface): + # Draw function + if Camera.instance: # If a camera instance is created + self.camera_rect = Camera.instance.apply(self) # Apply the rect of the current instance + + if self.tag == 'player': # If it's the player + self.get_status() # Get the status + if self.condition == 'jumping': + self.model = pygame.transform.scale(pygame.image.load(config.jumping[0]).convert_alpha(), (config.PLAYER_SIZE[0], config.PLAYER_SIZE[1])) # Jumping model + elif self.condition == 'falling': + self.model = pygame.transform.scale(pygame.image.load(config.falling[2]).convert_alpha(), (config.PLAYER_SIZE[0], config.PLAYER_SIZE[1])) # Falling model + elif self.condition == 'bonus_effect': + self.model = pygame.transform.scale(pygame.image.load(config.falling[3]).convert_alpha(), (config.PLAYER_SIZE[0], config.PLAYER_SIZE[1])) # Bonus effect model + + if self.direction == 'left': # Flip the image when moving in different directions + self.model = pygame.transform.flip(self.model, True, False) + + surface.blit(self.model, self.camera_rect) # Draw with the applied model + + if self.tag == 'bonus': # If it's a bonus + if not self.effect: # If not activated + self.model = pygame.transform.scale(pygame.image.load(config.bonus_default_image).convert_alpha(), (self.width, self.height)) # Default image + else: + if not self.animation_finished: # If animation is not finished + if self.bonus_effect_start_time is None: # Assign animation start time + self.bonus_effect_start_time = pygame.time.get_ticks() + + current_time = pygame.time.get_ticks() # Total time + elapsed_time = current_time - self.bonus_effect_start_time # Calculate remaining animation time + + if elapsed_time >= self.bonus_effect_duration: # If remaining time is greater than animation effect duration in ms + self.animation_finished = True # Finish animation + + # Determine current animation frame based on elapsed time and frame duration + self.current_frame = int(elapsed_time / (self.bonus_effect_duration / len(config.bonus_explosion))) + if self.current_frame >= len(config.bonus_explosion): + self.current_frame = len(config.bonus_explosion) - 1 + + self.model = pygame.image.load(config.bonus_explosion[self.current_frame]).convert_alpha() # Apply image to model + surface.blit(self.model, self.camera_rect) # Draw + + if self.tag == 'platform': # If it's a platform + surface.blit(pygame.transform.scale(self.model, (self.width, self.height)), self.camera_rect) # Draw the specified model + else: + surface.blit(self.model, self.rect) # If no camera instance is created yet, draw the instance itself first